diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/repository/JpaRepositoryAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/repository/JpaRepositoryAutoConfiguration.java index 00db0e5ce..65ad702c9 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/repository/JpaRepositoryAutoConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/repository/JpaRepositoryAutoConfiguration.java @@ -9,7 +9,7 @@ */ package org.eclipse.hawkbit.autoconfigure.repository; -import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.repository.jpa.JpaRepositoryConfiguration; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -22,8 +22,8 @@ import org.springframework.context.annotation.Import; * Auto-Configuration for enabling JPA repository. */ @Configuration -@ConditionalOnClass({ RepositoryApplicationConfiguration.class }) -@Import({ RepositoryApplicationConfiguration.class }) +@ConditionalOnClass({ JpaRepositoryConfiguration.class }) +@Import({ JpaRepositoryConfiguration.class }) public class JpaRepositoryAutoConfiguration { /** diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/scheduling/ExecutorAutoConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/scheduling/ExecutorAutoConfiguration.java index ab4809b05..ea13d603e 100644 --- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/scheduling/ExecutorAutoConfiguration.java +++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/scheduling/ExecutorAutoConfiguration.java @@ -50,7 +50,7 @@ public class ExecutorAutoConfiguration { /** * @return ExecutorService with security context availability in thread execution. */ - @Bean(destroyMethod = "shutdown") + @Bean(name = "asyncExecutor", destroyMethod = "shutdown") @ConditionalOnMissingBean public ExecutorService asyncExecutor() { return new DelegatingSecurityContextExecutorService(threadPoolExecutor()); 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 7472b9ef2..08defa6f5 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 @@ -17,7 +17,7 @@ import java.util.stream.Collectors; import org.eclipse.hawkbit.ContextAware; import org.eclipse.hawkbit.audit.AuditContextProvider; import org.eclipse.hawkbit.audit.AuditLoggingAspect; -import org.eclipse.hawkbit.im.authentication.SpRole; +import org.eclipse.hawkbit.repository.RepositoryConfiguration; import org.eclipse.hawkbit.tenancy.TenantAware.DefaultTenantResolver; import org.eclipse.hawkbit.tenancy.TenantAware.TenantResolver; import org.eclipse.hawkbit.tenancy.TenantAwareUserProperties; @@ -39,11 +39,9 @@ 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; +import org.springframework.context.annotation.Import; import org.springframework.data.domain.AuditorAware; -import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; -import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; -import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.security.web.authentication.logout.LogoutHandler; @@ -57,6 +55,7 @@ import org.springframework.util.CollectionUtils; */ @Configuration @EnableConfigurationProperties({ SecurityProperties.class, HawkbitSecurityProperties.class, TenantAwareUserProperties.class }) +@Import(RepositoryConfiguration.class) public class SecurityAutoConfiguration { @Bean @@ -170,19 +169,4 @@ public class SecurityAutoConfiguration { simpleUrlLogoutSuccessHandler.setTargetUrlParameter("login"); return simpleUrlLogoutSuccessHandler; } - - @Bean - @ConditionalOnMissingBean - static RoleHierarchy roleHierarchy() { - return RoleHierarchyImpl.fromHierarchy(SpRole.DEFAULT_ROLE_HIERARCHY); - } - - // and, if using method security also add - @Bean - @ConditionalOnMissingBean - static MethodSecurityExpressionHandler methodSecurityExpressionHandler(final RoleHierarchy roleHierarchy) { - final DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler(); - expressionHandler.setRoleHierarchy(roleHierarchy); - return expressionHandler; - } } \ No newline at end of file diff --git a/hawkbit-ddi/hawkbit-ddi-api/pom.xml b/hawkbit-ddi/hawkbit-ddi-api/pom.xml index b84813f68..bc630afb6 100644 --- a/hawkbit-ddi/hawkbit-ddi-api/pom.xml +++ b/hawkbit-ddi/hawkbit-ddi-api/pom.xml @@ -34,10 +34,6 @@ ${project.version} - - org.springframework.hateoas - spring-hateoas - org.springdoc springdoc-openapi-starter-webmvc-ui diff --git a/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java b/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java index f38b2672b..0653d8063 100644 --- a/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java +++ b/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/AbstractDDiApiIntegrationTest.java @@ -38,7 +38,7 @@ import org.eclipse.hawkbit.ddi.json.model.DdiConfirmationFeedback; import org.eclipse.hawkbit.ddi.json.model.DdiProgress; import org.eclipse.hawkbit.ddi.json.model.DdiResult; import org.eclipse.hawkbit.ddi.json.model.DdiStatus; -import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.repository.jpa.JpaRepositoryConfiguration; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Artifact; @@ -55,7 +55,7 @@ import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @ContextConfiguration( - classes = { DdiApiConfiguration.class, RestConfiguration.class, RepositoryApplicationConfiguration.class, TestConfiguration.class }) + classes = { DdiApiConfiguration.class, RestConfiguration.class, JpaRepositoryConfiguration.class, TestConfiguration.class }) @TestPropertySource(locations = "classpath:/ddi-test.properties") public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrationTest { diff --git a/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java b/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java index c410a269b..6ad013c3d 100644 --- a/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java +++ b/hawkbit-ddi/hawkbit-ddi-resource/src/test/java/org/eclipse/hawkbit/ddi/rest/resource/DdiRootControllerTest.java @@ -12,6 +12,10 @@ package org.eclipse.hawkbit.ddi.rest.resource; import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS; import static org.eclipse.hawkbit.im.authentication.SpPermission.TENANT_CONFIGURATION; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.callAs; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.getAs; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withController; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withUser; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.greaterThanOrEqualTo; @@ -55,7 +59,6 @@ import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; -import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch; import org.eclipse.hawkbit.repository.test.util.WithUser; import org.eclipse.hawkbit.rest.util.JsonBuilder; import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; @@ -126,7 +129,7 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { final Target findTargetByControllerID = targetManagement.getByControllerID(knownTargetControllerId).get(); assertThat(findTargetByControllerID.getCreatedBy()).isEqualTo(knownCreatedBy); // make a poll, audit information should not be changed, run as controller principal! - SecurityContextSwitch.runAs(SecurityContextSwitch.withController("controller", CONTROLLER_ROLE_ANONYMOUS), + callAs(withController("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), knownTargetControllerId)) .andDo(MockMvcResultPrinter.print()) @@ -198,8 +201,8 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Expect(type = TenantConfigurationCreatedEvent.class, count = 1), @Expect(type = TenantConfigurationDeletedEvent.class, count = 1) }) void pollWithModifiedGlobalPollingTime() throws Exception { - withPollingTime("00:02:00", () -> SecurityContextSwitch.runAs( - SecurityContextSwitch.withUser("controller", CONTROLLER_ROLE_ANONYMOUS), + withPollingTime("00:02:00", () -> callAs( + withUser("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711)) .andDo(MockMvcResultPrinter.print()) @@ -221,8 +224,8 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { @Expect(type = TenantConfigurationCreatedEvent.class, count = 1), @Expect(type = TenantConfigurationDeletedEvent.class, count = 1) }) void pollWithModifiedWithOverridesGlobalPollingTime() throws Exception { - withPollingTime("00:02:00, controllerid == 4711 -> 00:01:00", () -> SecurityContextSwitch.runAs( - SecurityContextSwitch.withUser("controller", CONTROLLER_ROLE_ANONYMOUS), + withPollingTime("00:02:00, controllerid == 4711 -> 00:01:00", () -> callAs( + withUser("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), 4711)) .andDo(MockMvcResultPrinter.print()) @@ -363,7 +366,7 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { final String knownControllerId1 = "0815"; final long create = System.currentTimeMillis(); // make a poll, audit information should be set on plug and play - SecurityContextSwitch.runAs(SecurityContextSwitch.withController("controller", CONTROLLER_ROLE_ANONYMOUS), + callAs(withController("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { mvc.perform(get(CONTROLLER_BASE, tenantAware.getCurrentTenant(), knownControllerId1)) .andDo(MockMvcResultPrinter.print()) @@ -582,7 +585,7 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { void sleepTimeResponseForDifferentMaintenanceWindowParameters() throws Exception { final DistributionSet ds = testdataFactory.createDistributionSet(""); - SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("tenantadmin", TENANT_CONFIGURATION), + getAs(withUser("tenantadmin", TENANT_CONFIGURATION), () -> { tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.POLLING_TIME, "00:05:00"); return null; @@ -751,7 +754,7 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { } private void withPollingTime(final String pollingTime, final Callable runnable) throws Exception { - SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("tenantadmin", TENANT_CONFIGURATION), + getAs(withUser("tenantadmin", TENANT_CONFIGURATION), () -> { tenantConfigurationManagement.addOrUpdateConfiguration(TenantConfigurationKey.POLLING_TIME, pollingTime); return null; @@ -759,7 +762,7 @@ class DdiRootControllerTest extends AbstractDDiApiIntegrationTest { try { runnable.call(); } finally { - SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("tenantadmin", TENANT_CONFIGURATION), + getAs(withUser("tenantadmin", TENANT_CONFIGURATION), () -> { tenantConfigurationManagement.deleteConfiguration(TenantConfigurationKey.POLLING_TIME); return null; diff --git a/hawkbit-ddi/hawkbit-ddi-resource/src/test/resources/ddi-test.properties b/hawkbit-ddi/hawkbit-ddi-resource/src/test/resources/ddi-test.properties index 6205e6406..2db581c1c 100644 --- a/hawkbit-ddi/hawkbit-ddi-resource/src/test/resources/ddi-test.properties +++ b/hawkbit-ddi/hawkbit-ddi-resource/src/test/resources/ddi-test.properties @@ -8,6 +8,10 @@ # SPDX-License-Identifier: EPL-2.0 # +# Logging START - activate to see request/response details +#logging.level.org.eclipse.hawkbit.rest.util.MockMvcResultPrinter=DEBUG +# Logging END + # DDI configuration - START hawkbit.controller.pollingTime=00:01:00 hawkbit.controller.pollingOverdueTime=00:01:00 @@ -21,6 +25,6 @@ spring.servlet.multipart.max-file-size=5MB hawkbit.server.security.dos.maxStatusEntriesPerAction=100 hawkbit.server.security.dos.maxAttributeEntriesPerTarget=10 # Quota - END -# Logging START - activate to see request/response details -#logging.level.org.eclipse.hawkbit.rest.util.MockMvcResultPrinter=DEBUG -# Logging END + +# disable spring cloud bus for tests +spring.cloud.bus.enabled=false diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java index 45bb93476..89da4eb29 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/AmqpMessageDispatcherServiceTest.java @@ -39,7 +39,7 @@ import org.eclipse.hawkbit.repository.event.remote.CancelTargetAssignmentEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAssignDistributionSetEvent; import org.eclipse.hawkbit.repository.event.remote.TargetAttributesRequestedEvent; import org.eclipse.hawkbit.repository.event.remote.TargetDeletedEvent; -import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.repository.jpa.JpaRepositoryConfiguration; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.Artifact; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -60,13 +60,17 @@ import org.springframework.amqp.support.converter.AbstractJavaTypeMapper; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; -@ActiveProfiles({ "test" }) /** * Feature: Component Tests - Device Management Federation API
* Story: AmqpMessage Dispatcher Service Test */ -@SpringBootTest(classes = { RepositoryApplicationConfiguration.class }, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@ActiveProfiles({ "test" }) +@SpringBootTest(classes = { JpaRepositoryConfiguration.class }, webEnvironment = SpringBootTest.WebEnvironment.NONE) +@TestPropertySource(properties = { + "spring.main.allow-bean-definition-overriding=true", + "spring.cloud.bus.enabled=true" }) class AmqpMessageDispatcherServiceTest extends AbstractIntegrationTest { private static final String TENANT = "DEFAULT"; 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 197a7a0e4..ee53135ee 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 @@ -71,15 +71,14 @@ import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConversionException; import org.springframework.amqp.support.converter.MessageConverter; -@ExtendWith(MockitoExtension.class) /** * Feature: Component Tests - Device Management Federation API
* Story: AmqpMessage Handler Service Test */ +@ExtendWith(MockitoExtension.class) class AmqpMessageHandlerServiceTest { - private static final String FAIL_MESSAGE_AMQP_REJECT_REASON = AmqpRejectAndDontRequeueException.class - .getSimpleName() + " was expected, "; + private static final String FAIL_MESSAGE_AMQP_REJECT_REASON = AmqpRejectAndDontRequeueException.class.getSimpleName() + " was expected, "; private static final String VIRTUAL_HOST = "vHost"; private static final String TENANT = "DEFAULT"; diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/BaseAmqpServiceTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/BaseAmqpServiceTest.java index ae10258c0..9f84a9a7d 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/BaseAmqpServiceTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/amqp/BaseAmqpServiceTest.java @@ -31,11 +31,11 @@ import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConversionException; -@ExtendWith(MockitoExtension.class) /** * Feature: Component Tests - Device Management Federation API
* Story: Base Amqp Service Test */ +@ExtendWith(MockitoExtension.class) class BaseAmqpServiceTest { @Mock diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java index b87d879bc..1a29b4e1d 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AbstractAmqpServiceIntegrationTest.java @@ -42,7 +42,7 @@ import org.eclipse.hawkbit.integration.listener.ReplyToListener; import org.eclipse.hawkbit.matcher.SoftwareModuleJsonMatcher; import org.eclipse.hawkbit.rabbitmq.test.AbstractAmqpIntegrationTest; import org.eclipse.hawkbit.rabbitmq.test.AmqpTestConfiguration; -import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.repository.jpa.JpaRepositoryConfiguration; import org.eclipse.hawkbit.repository.model.Action; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetAssignmentResult; @@ -66,7 +66,7 @@ import org.springframework.util.CollectionUtils; */ @ContextConfiguration(classes = { DmfApiConfiguration.class, DmfTestConfiguration.class, - RepositoryApplicationConfiguration.class, AmqpTestConfiguration.class }) + JpaRepositoryConfiguration.class, AmqpTestConfiguration.class }) abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpIntegrationTest { protected static final String TENANT_EXIST = "DEFAULT"; @@ -93,9 +93,9 @@ abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpIntegratio } protected T waitUntilIsPresent(final Callable> callable) { - await().until(() -> SecurityContextSwitch.runAsPrivileged(() -> callable.call().isPresent())); + await().until(() -> SecurityContextSwitch.callAsPrivileged(() -> callable.call().isPresent())); try { - return SecurityContextSwitch.runAsPrivileged(() -> callable.call().get()); + return SecurityContextSwitch.callAsPrivileged(() -> callable.call().get()); } catch (final Exception e) { return null; } @@ -367,7 +367,7 @@ abstract class AbstractAmqpServiceIntegrationTest extends AbstractAmqpIntegratio waitUntilIsPresent(() -> controllerManagement.getByControllerId(controllerId)); await().untilAsserted(() -> { try { - final Map controllerAttributes = SecurityContextSwitch.runAsPrivileged( + final Map controllerAttributes = SecurityContextSwitch.callAsPrivileged( () -> targetManagement.getControllerAttributes(controllerId)); assertThat(controllerAttributes).hasSameSizeAs(attributes); assertThat(controllerAttributes).containsAllEntriesOf(attributes); diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java index 85cd8f72a..ce6e31a93 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageDispatcherServiceIntegrationTest.java @@ -794,7 +794,7 @@ class AmqpMessageDispatcherServiceIntegrationTest extends AbstractAmqpServiceInt } private void waitUntil(final Callable callable) { - await().until(() -> SecurityContextSwitch.runAsPrivileged(callable)); + await().until(() -> SecurityContextSwitch.callAsPrivileged(callable)); } private void assertLatestMultiActionMessageContainsInstallMessages(final String controllerId, diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java index dafae26fc..f98f54e89 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/test/java/org/eclipse/hawkbit/integration/AmqpMessageHandlerServiceIntegrationTest.java @@ -1231,7 +1231,7 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr private void assertAction(final Long actionId, final int messages, final Status... expectedActionStates) { await().untilAsserted(() -> { try { - SecurityContextSwitch.runAsPrivileged(() -> { + SecurityContextSwitch.callAsPrivileged(() -> { final List actionStatusList = deploymentManagement.findActionStatusByAction(actionId, PAGE).getContent(); // Check correlation ID @@ -1265,7 +1265,7 @@ class AmqpMessageHandlerServiceIntegrationTest extends AbstractAmqpServiceIntegr final Status... expectedActionStates) { await().untilAsserted(() -> { try { - SecurityContextSwitch.runAsPrivileged(() -> { + SecurityContextSwitch.callAsPrivileged(() -> { final List actionStatusList = deploymentManagement .findActionStatusByAction(actionId, PAGE).getContent(); assertThat(actionStatusList).hasSize(statusListCount); diff --git a/hawkbit-dmf/hawkbit-dmf-rabbitmq-test/src/main/java/org/eclipse/hawkbit/rabbitmq/test/AbstractAmqpIntegrationTest.java b/hawkbit-dmf/hawkbit-dmf-rabbitmq-test/src/main/java/org/eclipse/hawkbit/rabbitmq/test/AbstractAmqpIntegrationTest.java index 82f52e069..b0946cd32 100644 --- a/hawkbit-dmf/hawkbit-dmf-rabbitmq-test/src/main/java/org/eclipse/hawkbit/rabbitmq/test/AbstractAmqpIntegrationTest.java +++ b/hawkbit-dmf/hawkbit-dmf-rabbitmq-test/src/main/java/org/eclipse/hawkbit/rabbitmq/test/AbstractAmqpIntegrationTest.java @@ -15,7 +15,7 @@ import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; import org.awaitility.Awaitility; import org.awaitility.core.ConditionFactory; -import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.repository.jpa.JpaRepositoryConfiguration; import org.eclipse.hawkbit.repository.test.TestConfiguration; import org.eclipse.hawkbit.repository.test.util.AbstractIntegrationTest; import org.junit.jupiter.api.BeforeEach; @@ -30,12 +30,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; @Slf4j @RabbitAvailable -@ContextConfiguration(classes = { RepositoryApplicationConfiguration.class, AmqpTestConfiguration.class, TestConfiguration.class }) +@ContextConfiguration(classes = { JpaRepositoryConfiguration.class, AmqpTestConfiguration.class, TestConfiguration.class }) // Dirty context is necessary to create a new vhost and recreate all necessary beans after every test class. @DirtiesContext(classMode = ClassMode.AFTER_CLASS) +@TestPropertySource(properties = { + "spring.main.allow-bean-definition-overriding=true", + "spring.cloud.bus.enabled=true" }) @SuppressWarnings("java:S6813") // constructor injects are not possible for test classes public abstract class AbstractAmqpIntegrationTest extends AbstractIntegrationTest { @@ -60,6 +64,7 @@ public abstract class AbstractAmqpIntegrationTest extends AbstractIntegrationTes private static final Duration AT_LEAST = Duration.ofMillis(Integer.getInteger("hawkbit.it.amqp.await.atLeastMs", 100)); private static final Duration POLL_INTERVAL = Duration.ofMillis(Integer.getInteger("hawkbit.it.amqp.await.pollIntervalMs", 200)); private static final Duration TIMEOUT = Duration.ofMillis(Integer.getInteger("hawkbit.it.amqp.await.timeoutMs", 5000)); + @Override protected ConditionFactory await() { return Awaitility.await().atLeast(AT_LEAST).pollInterval(POLL_INTERVAL).atMost(TIMEOUT); diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtApiConfiguration.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtApiConfiguration.java index c7734d58e..991e813c0 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtApiConfiguration.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtApiConfiguration.java @@ -19,13 +19,12 @@ import org.springframework.security.config.annotation.method.configuration.Enabl import org.springframework.stereotype.Controller; /** - * Enable {@link ComponentScan} in the resource package to setup all - * {@link Controller} annotated classes and setup the REST-Resources for the - * Management API. + * Enable {@link ComponentScan} in the resource package to set up all {@link Controller} annotated classes and set up the REST-Resources + * for the Management API. */ @Configuration @EnableMethodSecurity(proxyTargetClass = true, securedEnabled = true) @ComponentScan @Import({ RestConfiguration.class, OpenApi.class }) @PropertySource("classpath:/hawkbit-mgmt-api-defaults.properties") -public class MgmtApiConfiguration {} +public class MgmtApiConfiguration {} \ No newline at end of file diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/AbstractManagementApiIntegrationTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/AbstractManagementApiIntegrationTest.java index 2a8be887a..68a851cc0 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/AbstractManagementApiIntegrationTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/AbstractManagementApiIntegrationTest.java @@ -15,7 +15,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import lombok.SneakyThrows; import org.eclipse.hawkbit.mgmt.json.model.distributionset.MgmtActionType; -import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.repository.jpa.JpaRepositoryConfiguration; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.model.BaseEntity; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -32,7 +32,7 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.ResultMatcher; @ContextConfiguration( - classes = { MgmtApiConfiguration.class, RestConfiguration.class, RepositoryApplicationConfiguration.class, TestConfiguration.class }) + classes = { MgmtApiConfiguration.class, RestConfiguration.class, JpaRepositoryConfiguration.class, TestConfiguration.class }) @TestPropertySource(locations = "classpath:/mgmt-test.properties") public abstract class AbstractManagementApiIntegrationTest extends AbstractRestIntegrationTest { diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtBasicAuthResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtBasicAuthResourceTest.java index 26dde90d1..95f270487 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtBasicAuthResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtBasicAuthResourceTest.java @@ -20,7 +20,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.util.Base64; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; -import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.repository.jpa.JpaRepositoryConfiguration; import org.eclipse.hawkbit.repository.test.TestConfiguration; import org.eclipse.hawkbit.repository.test.matcher.EventVerifier; import org.eclipse.hawkbit.repository.test.util.CleanupTestExecutionListener; @@ -67,7 +67,7 @@ import org.springframework.web.context.WebApplicationContext; @WebAppConfiguration @AutoConfigureMockMvc @ContextConfiguration(classes = { MgmtApiConfiguration.class, RestConfiguration.class, - RepositoryApplicationConfiguration.class, TestConfiguration.class }) + JpaRepositoryConfiguration.class, TestConfiguration.class }) /** * Feature: Component Tests - Management API
* Story: Basic auth Userinfo Resource diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java index c5c8bfaaa..17453d7ea 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetResourceTest.java @@ -408,7 +408,7 @@ class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegrationTe try { payload.put(new JSONObject().put("id", trg.getId())); } catch (final JSONException e) { - e.printStackTrace(); + throw new IllegalStateException(e); } }); @@ -459,7 +459,7 @@ class MgmtDistributionSetResourceTest extends AbstractManagementApiIntegrationTe try { list.put(new JSONObject().put("id", target.getControllerId())); } catch (final JSONException e) { - e.printStackTrace(); + throw new IllegalStateException(e); } }); diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java index 623b81f8c..a57a793a9 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtRolloutResourceTest.java @@ -1912,13 +1912,13 @@ class MgmtRolloutResourceTest extends AbstractManagementApiIntegrationTest { private void awaitRunningState(final Long rolloutId) { awaitRollout().until(() -> SecurityContextSwitch - .runAsPrivileged(() -> rolloutManagement.get(rolloutId).orElseThrow(NoSuchElementException::new)) + .callAsPrivileged(() -> rolloutManagement.get(rolloutId).orElseThrow(NoSuchElementException::new)) .getStatus().equals(RolloutStatus.RUNNING)); } private void awaitActionStatus(final Long actionId, final Status status) { awaitRollout().until(() -> SecurityContextSwitch - .runAsPrivileged(() -> deploymentManagement.findAction(actionId).orElseThrow(NoSuchElementException::new)) + .callAsPrivileged(() -> deploymentManagement.findAction(actionId).orElseThrow(NoSuchElementException::new)) .getStatus().equals(status)); } diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java index a3f10bac7..7209ca194 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTenantManagementResourceTest.java @@ -9,6 +9,9 @@ */ package org.eclipse.hawkbit.mgmt.rest.resource; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.callAs; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.getAs; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withUser; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -22,7 +25,6 @@ import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.mgmt.json.model.system.MgmtSystemTenantConfigurationValueRequest; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.repository.model.DistributionSetType; -import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch; import org.eclipse.hawkbit.rest.util.MockMvcResultPrinter; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties; import org.json.JSONObject; @@ -132,7 +134,7 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg * Update DefaultDistributionSetType Fails if given DistributionSetType ID does not exist. */ @Test - void putTenantMetadataFails() throws Exception { + void putTenantMetadataFails() throws Exception { long oldDefaultDsType = getActualDefaultDsType(); //try an invalid input String newDefaultDsType = new JSONObject().put("value", true).toString(); @@ -149,7 +151,7 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg * The 'multi.assignments.enabled' property must not be changed to false. */ @Test - void deactivateMultiAssignment() throws Exception { + void deactivateMultiAssignment() throws Exception { final String bodyActivate = new JSONObject().put("value", true).toString(); final String bodyDeactivate = new JSONObject().put("value", false).toString(); @@ -168,7 +170,7 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg * The Batch configuration should not be applied, because of invalid TenantConfiguration props */ @Test - void changeBatchConfigurationShouldFailOnInvalidTenantConfiguration() throws Exception { + void changeBatchConfigurationShouldFailOnInvalidTenantConfiguration() throws Exception { //in this scenario // some TenantConfiguration are not valid, // TenantMetadata - DefaultDSType ID is valid, @@ -185,7 +187,7 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg * The Batch configuration should not be applied, because of invalid TenantMetadata (DefaultDistributionSetType) */ @Test - void changeBatchConfigurationShouldOnInvalidTenantMetadata() throws Exception { + void changeBatchConfigurationShouldOnInvalidTenantMetadata() throws Exception { //in this scenario // all TenantConfiguration have valid and new values - using old values, inverted // TenantMetadata - DefaultDSType ID is invalid @@ -218,7 +220,7 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg * The Batch configuration should be applied */ @Test - void changeBatchConfiguration() throws Exception { + void changeBatchConfiguration() throws Exception { long updatedDistributionSetType = createTestDistributionSetType(); boolean updatedRolloutApprovalEnabled = true; boolean updatedAuthGatewayTokenEnabled = true; @@ -253,7 +255,7 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg * The 'repository.actions.autoclose.enabled' property must not be modified if Multi-Assignments is enabled. */ @Test - void autoCloseCannotBeModifiedIfMultiAssignmentIsEnabled() throws Exception { + void autoCloseCannotBeModifiedIfMultiAssignmentIsEnabled() throws Exception { final String bodyActivate = new JSONObject().put("value", true).toString(); final String bodyDeactivate = new JSONObject().put("value", false).toString(); @@ -280,7 +282,7 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg * Handles DELETE request deleting a tenant specific configuration. */ @Test - void deleteTenantConfiguration() throws Exception { + void deleteTenantConfiguration() throws Exception { mvc.perform(delete(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs/{keyName}", TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY)) .andDo(MockMvcResultPrinter.print()) @@ -291,7 +293,7 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg * Tests DELETE request must Fail for TenantMetadata properties. */ @Test - void deleteTenantMetadataFail() throws Exception { + void deleteTenantMetadataFail() throws Exception { mvc.perform(delete(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs/{keyName}", DEFAULT_DISTRIBUTION_SET_TYPE_KEY)) .andDo(MockMvcResultPrinter.print()) @@ -303,18 +305,15 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg */ @Test void getTenantConfigurationReadGWToken() throws Exception { - SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("tenant_admin", SpPermission.TENANT_CONFIGURATION), () -> { + getAs(withUser("tenant_admin", SpPermission.TENANT_CONFIGURATION), () -> { tenantConfigurationManagement.addOrUpdateConfiguration( - TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, - "123"); + TenantConfigurationProperties.TenantConfigurationKey.AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY, "123"); return null; }); // TODO - should be able to read with TENANT_CONFIGURATION but somehow here the role hierarchy doesn't play // checked in mgmt / update server runtime PreAuthorizeEnabledTest - SecurityContextSwitch.runAs( - SecurityContextSwitch.withUser("tenant_admin", SpPermission.READ_TENANT_CONFIGURATION, SpPermission.READ_GATEWAY_SEC_TOKEN), - () -> { + callAs(withUser("tenant_admin", SpPermission.READ_TENANT_CONFIGURATION, SpPermission.READ_GATEWAY_SEC_TOKEN), () -> { mvc.perform(get(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs")) .andDo(MockMvcResultPrinter.print()) .andDo(m -> System.out.println("-> 1: " + m.getResponse().getContentAsString())) @@ -324,7 +323,7 @@ public class MgmtTenantManagementResourceTest extends AbstractManagementApiInteg return null; }); - SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("tenant_read", SpPermission.READ_TENANT_CONFIGURATION), () -> { + callAs(withUser("tenant_read", SpPermission.READ_TENANT_CONFIGURATION), () -> { mvc.perform(get(MgmtRestConstants.SYSTEM_V1_REQUEST_MAPPING + "/configs")) .andDo(MockMvcResultPrinter.print()) .andDo(m -> System.out.println("-> 2: " + m.getResponse().getContentAsString())) diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/resources/mgmt-test.properties b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/resources/mgmt-test.properties index b685a72f9..29803bb9a 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/resources/mgmt-test.properties +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/resources/mgmt-test.properties @@ -12,3 +12,5 @@ #logging.level.org.eclipse.hawkbit.rest.util.MockMvcResultPrinter=DEBUG # Logging END +# disable spring cloud bus for tests +spring.cloud.bus.enabled=false \ No newline at end of file diff --git a/hawkbit-mgmt/hawkbit-mgmt-server/src/test/java/org/eclipse/hawkbit/app/mgmt/PreAuthorizeEnabledTest.java b/hawkbit-mgmt/hawkbit-mgmt-server/src/test/java/org/eclipse/hawkbit/app/mgmt/PreAuthorizeEnabledTest.java index 06efb14be..1d1739302 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-server/src/test/java/org/eclipse/hawkbit/app/mgmt/PreAuthorizeEnabledTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-server/src/test/java/org/eclipse/hawkbit/app/mgmt/PreAuthorizeEnabledTest.java @@ -11,6 +11,7 @@ package org.eclipse.hawkbit.app.mgmt; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import java.util.HashMap; @@ -47,6 +48,32 @@ class PreAuthorizeEnabledTest extends AbstractSecurityTest { assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value())); } + /** + * Tests whether request returns distribution set if a role with scope is granted for the user + */ + @Test + @WithUser(authorities = { SpPermission.CREATE_REPOSITORY, SpPermission.READ_REPOSITORY + "/name==DsOne" }, autoCreateTenant = false) + void successIfHasRoleWithScope() throws Exception { + createDsOne("successIfHasRoleWithScope"); + mvc.perform(get("/rest/v1/distributionsets")).andExpect(result -> { + assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(result.getResponse().getContentAsString()).contains("DsOne"); + }); + } + + /** + * Tests whether request doesn't return distribution set if a role with scope doesn't grant access + */ + @Test + @WithUser(authorities = { SpPermission.CREATE_REPOSITORY, SpPermission.READ_REPOSITORY + "/name==DsOne2" }, autoCreateTenant = false) + void failIfHasNoForbiddingScope() throws Exception { + createDsOne("failIfHasNoForbiddingScope"); + mvc.perform(get("/rest/v1/distributionsets")).andExpect(result -> { + assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(result.getResponse().getContentAsString()).doesNotContain("DsOne"); + }); + } + /** * Tests whether request succeed if a role is granted for the user */ @@ -80,4 +107,17 @@ class PreAuthorizeEnabledTest extends AbstractSecurityTest { mvc.perform(get("/rest/v1/system/configs")).andExpect(result -> assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value())); } + + private void createDsOne(final String version) throws Exception { + mvc.perform(post("/rest/v1/distributionsets") + .header("Content-Type", "application/json") + .content(""" + [ + { + "name": "DsOne", + "version": "${version}" + } + ]""".replace("${version}", version))) + .andExpect(result -> assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.CREATED.value())); + } } \ No newline at end of file diff --git a/hawkbit-monolith/hawkbit-update-server/src/test/java/org/eclipse/hawkbit/app/PreAuthorizeEnabledTest.java b/hawkbit-monolith/hawkbit-update-server/src/test/java/org/eclipse/hawkbit/app/PreAuthorizeEnabledTest.java index 71aeb62b5..271a22a2e 100644 --- a/hawkbit-monolith/hawkbit-update-server/src/test/java/org/eclipse/hawkbit/app/PreAuthorizeEnabledTest.java +++ b/hawkbit-monolith/hawkbit-update-server/src/test/java/org/eclipse/hawkbit/app/PreAuthorizeEnabledTest.java @@ -11,6 +11,7 @@ package org.eclipse.hawkbit.app; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import java.util.HashMap; @@ -47,6 +48,32 @@ class PreAuthorizeEnabledTest extends AbstractSecurityTest { .andExpect(result -> assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value())); } + /** + * Tests whether request returns distribution set if a role with scope is granted for the user + */ + @Test + @WithUser(authorities = { SpPermission.CREATE_REPOSITORY, SpPermission.READ_REPOSITORY + "/name==DsOne" }, autoCreateTenant = false) + void successIfHasRoleWithScope() throws Exception { + createDsOne("successIfHasRoleWithScope"); + mvc.perform(get("/rest/v1/distributionsets")).andExpect(result -> { + assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(result.getResponse().getContentAsString()).contains("DsOne"); + }); + } + + /** + * Tests whether request doesn't return distribution set if a role with scope doesn't grant access + */ + @Test + @WithUser(authorities = { SpPermission.CREATE_REPOSITORY, SpPermission.READ_REPOSITORY + "/name==DsOne2" }, autoCreateTenant = false) + void failIfHasNoForbiddingScope() throws Exception { + createDsOne("failIfHasNoForbiddingScope"); + mvc.perform(get("/rest/v1/distributionsets")).andExpect(result -> { + assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value()); + assertThat(result.getResponse().getContentAsString()).doesNotContain("DsOne"); + }); + } + /** * Tests whether request succeed if a role is granted for the user */ @@ -58,7 +85,7 @@ class PreAuthorizeEnabledTest extends AbstractSecurityTest { } /** - * Tests whether read tenant config request fail if a tenant config (or read read) is not granted for the user + * Tests whether read tenant config request fail if a tenant config (or read) is not granted for the user */ @Test @WithUser(authorities = { SpPermission.READ_TARGET }, autoCreateTenant = false) @@ -81,4 +108,17 @@ class PreAuthorizeEnabledTest extends AbstractSecurityTest { mvc.perform(get("/rest/v1/system/configs")) .andExpect(result -> assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.OK.value())); } + + private void createDsOne(final String version) throws Exception { + mvc.perform(post("/rest/v1/distributionsets") + .header("Content-Type", "application/json") + .content(""" + [ + { + "name": "DsOne", + "version": "${version}" + } + ]""".replace("${version}", version))) + .andExpect(result -> assertThat(result.getResponse().getStatus()).isEqualTo(HttpStatus.CREATED.value())); + } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/pom.xml b/hawkbit-repository/hawkbit-repository-api/pom.xml index f0062cc73..5490eab1f 100644 --- a/hawkbit-repository/hawkbit-repository-api/pom.xml +++ b/hawkbit-repository/hawkbit-repository-api/pom.xml @@ -33,10 +33,6 @@ ${project.version}
- - org.springframework.hateoas - spring-hateoas - org.springframework.cloud spring-cloud-starter-bus-amqp @@ -55,10 +51,6 @@ com.cronutils cron-utils - - cz.jirutka.rsql - rsql-parser - org.jsoup jsoup diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/MaintenanceScheduleHelper.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/MaintenanceScheduleHelper.java index 9e958b4cc..145103f67 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/MaintenanceScheduleHelper.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/MaintenanceScheduleHelper.java @@ -21,46 +21,35 @@ import com.cronutils.model.CronType; import com.cronutils.model.definition.CronDefinitionBuilder; import com.cronutils.model.time.ExecutionTime; import com.cronutils.parser.CronParser; +import lombok.NoArgsConstructor; import org.eclipse.hawkbit.repository.exception.InvalidMaintenanceScheduleException; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** - * Helper class to check validity of maintenance schedule definition and manage - * scheduling of maintenance window using a cron expression based scheduler. It - * also provides a helper method for conversion of duration specified in - * HH:mm:ss format to ISO format. + * Helper class to check validity of maintenance schedule definition and manage scheduling of maintenance window using + * a cron expression based scheduler. It also provides a helper method for conversion of duration specified in HH:mm:ss format to ISO format. */ +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) public final class MaintenanceScheduleHelper { - private static final CronParser cronParser = new CronParser( - CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)); - - private MaintenanceScheduleHelper() { - throw new IllegalStateException("Utility class"); - } + private static final CronParser CRON_PARSER = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ)); /** * Calculate the next available maintenance window. * - * @param cronSchedule is a cron expression with 6 mandatory fields and 1 last - * optional field: "second minute hour dayofmonth month weekday - * year". - * @param duration in HH:mm:ss format specifying the duration of a maintenance - * window, for example 00:30:00 for 30 minutes. - * @param timezone is the time zone specified as +/-hh:mm offset from UTC. For - * example +02:00 for CET summer time and +00:00 for UTC. The - * start time of a maintenance window calculated based on the - * cron expression is relative to this time zone. - * @return { @link Optional} of the next available window. In - * case there is none, or there are maintenance window validation + * @param cronSchedule is a cron expression with 6 mandatory fields and 1 last optional field: "second minute hour day-of-month month + * weekday year". + * @param duration in HH:mm:ss format specifying the duration of a maintenance window, for example 00:30:00 for 30 minutes. + * @param timezone is the time zone specified as +/-hh:mm offset from UTC. For example +02:00 for CET summer time and +00:00 for UTC. The + * start time of a maintenance window calculated based on the cron expression is relative to this time zone. + * @return { @link Optional} of the next available window. In case there is none, or there are maintenance window validation * errors, returns empty value. */ // Exception squid:S1166 - if there are validation error(format of cron // expression or duration is wrong), we simply return empty value @SuppressWarnings("squid:S1166") - public static Optional getNextMaintenanceWindow(final String cronSchedule, final String duration, - final String timezone) { + public static Optional getNextMaintenanceWindow(final String cronSchedule, final String duration, final String timezone) { try { final ExecutionTime scheduleExecutor = ExecutionTime.forCron(getCronFromExpression(cronSchedule)); final ZonedDateTime now = ZonedDateTime.now(ZoneOffset.of(timezone)); @@ -74,36 +63,28 @@ public final class MaintenanceScheduleHelper { /** * Parse the given cron expression with quartz parser. * - * @param cronSchedule is a cron expression with 6 mandatory fields and 1 last - * optional field: "second minute hour dayofmonth month weekday - * year". + * @param cronSchedule is a cron expression with 6 mandatory fields and 1 last optional field: "second minute hour day-of-month + * month weekday year". * @return {@link Cron} object, that corresponds to the expression. * @throws IllegalArgumentException if the cron expression doesn't have a valid format. */ public static Cron getCronFromExpression(final String cronSchedule) { - return cronParser.parse(cronSchedule); + return CRON_PARSER.parse(cronSchedule); } /** - * Check if the maintenance schedule definition is valid in terms of - * validity of cron expression, duration and availability of at least one - * valid maintenance window. Further a maintenance schedule is valid if - * either all the parameters: schedule, duration and time zone are valid or - * are null. + * Check if the maintenance schedule definition is valid in terms of validity of cron expression, duration and availability of at least one + * valid maintenance window. Further a maintenance schedule is valid if either all the parameters: schedule, duration and time zone are + * valid or are null. * - * @param cronSchedule is a cron expression with 6 mandatory fields and 1 last - * optional field: "second minute hour dayofmonth month weekday - * year". - * @param duration in HH:mm:ss format specifying the duration of a maintenance - * window, for example 00:30:00 for 30 minutes. - * @param timezone is the time zone specified as +/-hh:mm offset from UTC. For - * example +02:00 for CET summer time and +00:00 for UTC. The - * start time of a maintenance window calculated based on the - * cron expression is relative to this time zone. + * @param cronSchedule is a cron expression with 6 mandatory fields and 1 last optional field: "second minute hour day-of-month month + * weekday year". + * @param duration in HH:mm:ss format specifying the duration of a maintenance window, for example 00:30:00 for 30 minutes. + * @param timezone is the time zone specified as +/-hh:mm offset from UTC. For example +02:00 for CET summer time and +00:00 for UTC. The + * start time of a maintenance window calculated based on the cron expression is relative to this time zone. * @throws InvalidMaintenanceScheduleException if the defined schedule fails the validity criteria. */ - public static void validateMaintenanceSchedule(final String cronSchedule, final String duration, - final String timezone) { + public static void validateMaintenanceSchedule(final String cronSchedule, final String duration, final String timezone) { if (allNotEmpty(cronSchedule, duration, timezone)) { validateCronSchedule(cronSchedule); validateDuration(duration); @@ -114,18 +95,15 @@ public final class MaintenanceScheduleHelper { "No valid maintenance window available after current time"); } } else if (atLeastOneNotEmpty(cronSchedule, duration, timezone)) { - throw new InvalidMaintenanceScheduleException( - "All of schedule, duration and timezone should either be null or non empty."); + throw new InvalidMaintenanceScheduleException("All of schedule, duration and timezone should either be null or non empty."); } } /** - * Convert the time interval or duration specified in "HH:mm:ss" format to - * ISO format. + * Convert the time interval or duration specified in "HH:mm:ss" format to ISO format. * - * @param timeInterval in "HH:mm:ss" string format. This format is popularly used but - * can be confused with time of the day, hence conversion to ISO - * specified format for time duration is required. + * @param timeInterval in "HH:mm:ss" string format. This format is popularly used but can be confused with time of the day, + * hence conversion to ISO specified format for time duration is required. * @return {@link Duration} in ISO format. * @throws DateTimeParseException if the text cannot be converted to ISO format. */ @@ -136,11 +114,9 @@ public final class MaintenanceScheduleHelper { /** * Validates the format of the maintenance window duration * - * @param duration in "HH:mm:ss" string format. This format is popularly used but - * can be confused with time of the day, hence conversion to ISO - * specified format for time duration is required. - * @throws InvalidMaintenanceScheduleException if the duration doesn't have a valid format to be converted - * to ISO. + * @param duration in "HH:mm:ss" string format. This format is popularly used but can be confused with time of the day, hence conversion + * to ISO specified format for time duration is required. + * @throws InvalidMaintenanceScheduleException if the duration doesn't have a valid format to be converted to ISO. */ public static void validateDuration(final String duration) { try { @@ -155,9 +131,8 @@ public final class MaintenanceScheduleHelper { /** * Validates the format of the maintenance window cron expression * - * @param cronSchedule is a cron expression with 6 mandatory fields and 1 last - * optional field: "second minute hour dayofmonth month weekday - * year". + * @param cronSchedule is a cron expression with 6 mandatory fields and 1 last optional field: "second minute hour day-of-month month + * weekday year". * @throws InvalidMaintenanceScheduleException if the cron expression doesn't have a valid quartz format. */ public static void validateCronSchedule(final String cronSchedule) { @@ -181,4 +156,4 @@ public final class MaintenanceScheduleHelper { private static LocalTime convertDurationToLocalTime(final String timeInterval) { return LocalTime.parse(timeInterval.strip()); } -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ValidString.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ValidString.java index 627092b96..5086f5370 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ValidString.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ValidString.java @@ -21,15 +21,11 @@ import jakarta.validation.Payload; * Constraint for strings submitted into the repository. */ @Constraint(validatedBy = ValidStringValidator.class) -@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, - ElementType.PARAMETER, ElementType.TYPE_USE }) +@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE }) @Retention(RetentionPolicy.RUNTIME) public @interface ValidString { String message() default "Invalid characters in string"; - Class[] groups() default {}; - Class[] payload() default {}; - -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/EventPublisherHolder.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/EventPublisherHolder.java index 1bf4e687d..4ce5ac11f 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/EventPublisherHolder.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/event/EventPublisherHolder.java @@ -49,7 +49,7 @@ public final class EventPublisherHolder { this.serviceMatcher = serviceMatcher; } - @Autowired // spring setter injection + @Autowired(required = false) // spring setter injection public void setBusProperties(final BusProperties bus) { this.bus = bus; } diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RepositoryConfiguration.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RepositoryConfiguration.java new file mode 100644 index 000000000..944d5e06a --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RepositoryConfiguration.java @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2015 Bosch Software Innovations GmbH and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.hawkbit.repository; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; +import org.eclipse.hawkbit.im.authentication.SpRole; +import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties; +import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.PropertySource; +import org.springframework.expression.EvaluationContext; +import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.access.expression.method.MethodSecurityExpressionOperations; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.util.function.SingletonSupplier; + +/** + * Default configuration that is common to all repository implementations. + */ +@Configuration +@EnableMethodSecurity(proxyTargetClass = true, securedEnabled = true) +@EnableConfigurationProperties({ RepositoryProperties.class, ControllerPollProperties.class, TenantConfigurationProperties.class }) +@PropertySource("classpath:/hawkbit-repository-defaults.properties") +public class RepositoryConfiguration { + + @Bean + @ConditionalOnMissingBean + static RoleHierarchy roleHierarchy() { + return RoleHierarchyImpl.fromHierarchy(SpRole.DEFAULT_ROLE_HIERARCHY); + } + + @Bean + @Primary + MethodSecurityExpressionHandler methodSecurityExpressionHandler( + final Optional roleHierarchy, final Optional applicationContext) { + final DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler = new DefaultMethodSecurityExpressionHandler() { + + @Override + public EvaluationContext createEvaluationContext(final Supplier authentication, final MethodInvocation mi) { + return super.createEvaluationContext(SingletonSupplier.of(() -> new RawAuthoritiesAuthentication(authentication.get())), mi); + } + + @Override + protected MethodSecurityExpressionOperations createSecurityExpressionRoot( + final Authentication authentication, final MethodInvocation mi) { + return super.createSecurityExpressionRoot(new RawAuthoritiesAuthentication(authentication), mi); + } + }; + roleHierarchy.ifPresent(methodSecurityExpressionHandler::setRoleHierarchy); + applicationContext.ifPresent(methodSecurityExpressionHandler::setApplicationContext); + return methodSecurityExpressionHandler; + } + + private static class RawAuthoritiesAuthentication implements Authentication { + + private final Authentication authentication; + private final transient SingletonSupplier> rawAuthoritiesSupplier; + + public RawAuthoritiesAuthentication(final Authentication authentication) { + this.authentication = authentication; + rawAuthoritiesSupplier = SingletonSupplier.of( + () -> authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority)// get the authority + .map(authority -> { + // permissions are in the format UPDATE_TARGET(/). + // here we remove the rsql query - not supported by expression evaluation + // the rsql evaluation will be done later by the access controller + final int index = authority.indexOf('/'); + return index < 0 ? authority : authority.substring(0, index); + }) + .distinct() // remove duplicates if any + .map(SimpleGrantedAuthority::new) + .toList()); + } + + @Override + public Collection getAuthorities() { + return rawAuthoritiesSupplier.get(); + } + + @Override + public Object getCredentials() { + return authentication.getCredentials(); + } + + @Override + public Object getDetails() { + return authentication.getDetails(); + } + + @Override + public Object getPrincipal() { + return authentication.getPrincipal(); + } + + @Override + public boolean isAuthenticated() { + return authentication.isAuthenticated(); + } + + @Override + public void setAuthenticated(final boolean isAuthenticated) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public String getName() { + return authentication.getName(); + } + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RepositoryDefaultConfiguration.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RepositoryDefaultConfiguration.java deleted file mode 100644 index 171f1c92c..000000000 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/RepositoryDefaultConfiguration.java +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright (c) 2015 Bosch Software Innovations GmbH and others - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.hawkbit.repository; - -import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties; -import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; - -/** - * Default configuration that is common to all repository implementations. - */ -@Configuration -@EnableMethodSecurity(proxyTargetClass = true, securedEnabled = true) -@EnableConfigurationProperties({ RepositoryProperties.class, ControllerPollProperties.class, TenantConfigurationProperties.class }) -@PropertySource("classpath:/hawkbit-repository-defaults.properties") -public class RepositoryDefaultConfiguration {} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/EntityMatcher.java b/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/EntityMatcher.java index 66c14b7d0..758774ef1 100644 --- a/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/EntityMatcher.java +++ b/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/EntityMatcher.java @@ -63,7 +63,7 @@ public class EntityMatcher { fieldGetter.setAccessible(true); final Object fieldValue = fieldGetter.invoke(t); final Operator op = comparison.getOp(); - if (Map.class.isAssignableFrom(fieldGetter.getReturnType())) { + if (Map.class.isAssignableFrom(getReturnType(fieldGetter))) { if ((op == NE || op == NOT_IN || op == NOT_LIKE) && (fieldValue == null || !((Map) fieldValue).containsKey(split[1]))) { // TODO / recheck - when missing entity shall it be included or not in != or =out=? - now it's not @@ -75,19 +75,19 @@ public class EntityMatcher { map( comparison.getValue(), (Class) ((ParameterizedType) fieldGetter.getGenericReturnType()).getActualTypeArguments()[1])); - } else if (Collection.class.isAssignableFrom(fieldGetter.getReturnType())) { // Set / List + } else if (Collection.class.isAssignableFrom(getReturnType(fieldGetter))) { // Set / List final Object value; final BiFunction compare; if (split.length == 1) { - value = map(comparison.getValue(), fieldGetter.getReturnType()); + value = map(comparison.getValue(), getReturnType(fieldGetter)); compare = (e, operator) -> compare(e, operator, value); } else { final Method valueGetter = getGetter( (Class) ((ParameterizedType) fieldGetter.getGenericReturnType()).getActualTypeArguments()[0], split[1]); - value = map(comparison.getValue(), valueGetter.getReturnType()); + value = map(comparison.getValue(), getReturnType(valueGetter)); compare = (e, operator) -> { try { - return compare(map(e == null ? null : valueGetter.invoke(e), valueGetter.getReturnType()), operator, value); + return compare(map(e == null ? null : valueGetter.invoke(e), getReturnType(valueGetter)), operator, value); } catch (final IllegalAccessException | InvocationTargetException ex) { throw new IllegalArgumentException(ex); } @@ -104,23 +104,23 @@ public class EntityMatcher { }; } else { if (split.length == 1) { - return compare(fieldValue, op, map(comparison.getValue(), fieldGetter.getReturnType())); + return compare(fieldValue, op, map(comparison.getValue(), getReturnType(fieldGetter))); } else { if (split[1].contains(".")) { // nested field access final String[] nestedSplit = split[1].split("\\.", 2); - final Method nestedFieldGetter = getGetter(fieldGetter.getReturnType(), nestedSplit[0]); + final Method nestedFieldGetter = getGetter(getReturnType(fieldGetter), nestedSplit[0]); nestedFieldGetter.setAccessible(true); - final Method valueGetter = getGetter(nestedFieldGetter.getReturnType(), nestedSplit[1]); + final Method valueGetter = getGetter(getReturnType(nestedFieldGetter), nestedSplit[1]); final Object nestedFieldValue = fieldValue == null ? null : nestedFieldGetter.invoke(fieldValue); return compare( nestedFieldValue == null ? null : valueGetter.invoke(nestedFieldValue), op, - map(comparison.getValue(), valueGetter.getReturnType())); + map(comparison.getValue(), getReturnType(valueGetter))); } else { - final Method valueGetter = getGetter(fieldGetter.getReturnType(), split[1]); + final Method valueGetter = getGetter(getReturnType(fieldGetter), split[1]); return compare(fieldValue == null ? null : valueGetter.invoke(fieldValue), op, - map(comparison.getValue(), valueGetter.getReturnType())); + map(comparison.getValue(), getReturnType(valueGetter))); } } } @@ -142,12 +142,24 @@ public class EntityMatcher { return Arrays.stream(t.getMethods()) .filter(method -> getterLowercase.equals(method.getName().toLowerCase())) .findFirst() - .map(method -> { - method.setAccessible(true); - return method; + .map(Method::getName) + .map(getterName -> { + try { + // gets method via Class.getMethod(String, Class...) because in listing it might has no the + // correct return type, but the type got from a declaring generic type + final Method getter = t.getMethod(getterName); + getter.setAccessible(true); + return getter; + } catch (final NoSuchMethodException e) { + throw new IllegalStateException("Unexpected: No getter found for field: " + fieldName + " in class: " + t.getName(), e); + } }).orElseThrow(() -> new NoSuchMethodException("No getter found for field: " + fieldName + " in class: " + t.getName())); } + private static Class getReturnType(final Method valueGetter) { + return valueGetter.getReturnType(); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) private static Object map(final Object value, final Class type) { if (value instanceof Collection collection) { // in / out 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/JpaRepositoryConfiguration.java similarity index 99% rename from hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java rename to hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRepositoryConfiguration.java index ae77993f4..e3c13a0c5 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/JpaRepositoryConfiguration.java @@ -36,7 +36,7 @@ import org.eclipse.hawkbit.repository.DistributionSetTypeManagement; import org.eclipse.hawkbit.repository.EntityFactory; import org.eclipse.hawkbit.repository.PropertiesQuotaManagement; import org.eclipse.hawkbit.repository.QuotaManagement; -import org.eclipse.hawkbit.repository.RepositoryDefaultConfiguration; +import org.eclipse.hawkbit.repository.RepositoryConfiguration; import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.RolloutApprovalStrategy; import org.eclipse.hawkbit.repository.RolloutExecutor; @@ -208,9 +208,9 @@ import org.springframework.validation.beanvalidation.MethodValidationPostProcess @EnableRetry @EntityScan("org.eclipse.hawkbit.repository.jpa.model") @PropertySource("classpath:/hawkbit-jpa-defaults.properties") -@Import({ JpaConfiguration.class, RepositoryDefaultConfiguration.class, LockProperties.class, DataSourceAutoConfiguration.class, SystemManagementCacheKeyGenerator.class }) +@Import({ JpaConfiguration.class, RepositoryConfiguration.class, LockProperties.class, DataSourceAutoConfiguration.class, SystemManagementCacheKeyGenerator.class }) @AutoConfigureAfter(DataSourceAutoConfiguration.class) -public class RepositoryApplicationConfiguration { +public class JpaRepositoryConfiguration { /** * Defines the validation processor bean. diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/acm/DefaultAccessController.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/acm/DefaultAccessController.java new file mode 100644 index 000000000..50069603b --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/acm/DefaultAccessController.java @@ -0,0 +1,167 @@ +/** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.hawkbit.repository.jpa.acm; + +import static org.eclipse.hawkbit.security.SecurityContextTenantAware.SYSTEM_USER; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.hawkbit.ContextAware; +import org.eclipse.hawkbit.repository.RsqlQueryField; +import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; +import org.eclipse.hawkbit.repository.jpa.ql.EntityMatcher; +import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.ObjectUtils; + +@Slf4j +public class DefaultAccessController & RsqlQueryField, T> implements AccessController { + + private final Class rsqlQueryFieldType; + private final Map> permissions = new EnumMap<>(Operation.class); + + @Value("${hawkbit.jpa.security.default-access-controller.strict:false}") + private boolean strict; + private ContextAware contextAware; + private RoleHierarchy roleHierarchy; + + public DefaultAccessController(final Class rsqlQueryFieldType, final String... permissionTypes) { + if (ObjectUtils.isEmpty(permissionTypes)) { + throw new IllegalArgumentException("Permission types must not be empty"); + } + + this.rsqlQueryFieldType = rsqlQueryFieldType; + for (final Operation operation : Operation.values()) { + for (final String permissionType : permissionTypes) { + permissions.computeIfAbsent(operation, k -> new ArrayList<>()).add(operation.name() + "_" + permissionType.toUpperCase()); + } + } + } + + @Autowired + void setContextAware(final ContextAware contextAware) { + this.contextAware = contextAware; + } + + @Autowired(required = false) + void setRoleHierarchy(final RoleHierarchy roleHierarchy) { + this.roleHierarchy = roleHierarchy; + } + + @Override + public Optional> getAccessRules(final Operation operation) { + if (contextAware.getCurrentTenant() != null && SYSTEM_USER.equals(contextAware.getCurrentUsername())) { + // as tenant, no restrictions + return Optional.empty(); + } + + return Optional.ofNullable(getScopes(operation)) // if get scopes returns null, no scopes return no spec - all entities are accessible + .map(scopes -> // to RSQL + scopes.size() == 1 + ? scopes.get(0) // single scope + : "(" + String.join(") or (", scopes) + ")") // join multiple scopes with 'or' - union + .map(scope -> RsqlUtility.getInstance().buildRsqlSpecification(scope, rsqlQueryFieldType)); + } + + @Override + public void assertOperationAllowed(final Operation operation, final T entity) throws InsufficientPermissionException { + if (contextAware.getCurrentTenant() != null && SYSTEM_USER.equals(contextAware.getCurrentUsername())) { + // as tenant, no restrictions + return; + } + + final List scopes = getScopes(operation); + if (scopes != null) { + for (final String scope : scopes) { + if (EntityMatcher.forRsql(scope).match(entity)) { + return; // at least one scope matches, operation is allowed + } + } + throw new InsufficientPermissionException(String.format("Operation '%s' is not allowed", operation)); + } // else if scopes is null, no scopes are defined, so all entities are accessible + } + + // returns null if ALL entities are accessible, otherwise returns a list of scopes + // throws InsufficientPermissionException if no matching authority found (should not happen - should be already checked with @PreAuthorize) + // java:S1168 - returns null with purpose to indicate no scopes, privately used with attention + // java:S1168 - better readable at one place + @SuppressWarnings({ "java:S1168", "java:S1168" }) + private List getScopes(final Operation operation) { + final List operationPermissions = permissions.get(operation); + final List scopes = SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .map(Permission::from) + .flatMap(permission -> roleHierarchy == null + ? (operationPermissions.contains(permission.name()) ? Stream.of(permission) : Stream.empty()) + : roleHierarchy.getReachableGrantedAuthorities(List.of(new SimpleGrantedAuthority(permission.name()))) + .stream() + .map(GrantedAuthority::getAuthority) + .filter(operationPermissions::contains) + .map(reachableAuthority -> new Permission(reachableAuthority, permission.scope()))) + .map(Permission::scope) + .distinct() // remove duplicates + .toList(); + if (scopes.isEmpty()) { + // no matching authority found for the operation + // the needed permission should have already been checked with @PreAuthorize + // could happen, for instance, in controller management, that checks ROLE_CONTROLLER and on its behalf + // calls pure repository methods as privileged + if (strict) { + throw new InsufficientPermissionException( + String.format( + "No matching authority found for operation %s" + + " (expects %s, should not happen - shall have already been checked with @PreAuthorize)", + operation, operationPermissions)); + } else { + // TODO - maybe in some future we could adapt permissions so controller roles to somehow apply what is needed + // and to do not "assume" and to throw exception always + log.debug( + "[{}] No matching authority found for operation {} (expects {}), they shall have already been checked with @PreAuthorize)", + rsqlQueryFieldType, operation, operationPermissions); + return null; + } + } else if (scopes.contains(null)) { + return null; // not scoped at all + } else { + return scopes; + } + } + + private record Permission(String name, String scope) { + + private static final Pattern PATTERN = Pattern.compile("^(?[^/]+)(/(?.+))?$"); + + static Permission from(final String authority) { + final Matcher matcher = PATTERN.matcher(authority); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid authority format: " + authority); + } + return from(matcher); + } + + static Permission from(final Matcher matcher) { + return new Permission(matcher.group("name"), matcher.group("scope")); + } + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/acm/DefaultAccessControllerConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/acm/DefaultAccessControllerConfiguration.java new file mode 100644 index 000000000..c47f6d03a --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/acm/DefaultAccessControllerConfiguration.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.hawkbit.repository.jpa.acm; + +import org.eclipse.hawkbit.repository.DistributionSetFields; +import org.eclipse.hawkbit.repository.TargetFields; +import org.eclipse.hawkbit.repository.TargetTypeFields; +import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConditionalOnProperty(name = "hawkbit.acm.access-controller.enabled", havingValue = "true", matchIfMissing = true) +public class DefaultAccessControllerConfiguration { + + @Bean + @ConditionalOnProperty(name = "hawkbit.acm.access-controller.target.enabled", havingValue = "true", matchIfMissing = true) + AccessController targetAccessController() { + return new DefaultAccessController<>(TargetFields.class, "TARGET"); + } + + @Bean + @ConditionalOnProperty(name = "hawkbit.acm.access-controller.target-type.enabled", havingValue = "true", matchIfMissing = true) + AccessController targetTypeAccessController() { + return new DefaultAccessController<>(TargetTypeFields.class, "TARGET", "TARGET_TYPE"); + } + + @Bean + @ConditionalOnProperty(name = "hawkbit.acm.access-controller.distribution-set.enabled", havingValue = "true", matchIfMissing = true) + AccessController distributionSetAccessController() { + return new DefaultAccessController<>(DistributionSetFields.class, "REPOSITORY", "DISTRIBUTION_SET"); + } +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/AbstractRemoteEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/AbstractRemoteEventTest.java index d8d5f14bf..e2a50a5f0 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/AbstractRemoteEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/AbstractRemoteEventTest.java @@ -29,6 +29,7 @@ import org.springframework.integration.support.MutableMessageHeaders; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.converter.AbstractMessageConverter; +import org.springframework.test.context.TestPropertySource; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; @@ -37,28 +38,22 @@ import org.springframework.util.MimeTypeUtils; /** * Test the remote entity events. */ +@TestPropertySource(properties = { "spring.cloud.bus.enabled=true" }) @SuppressWarnings("java:S6813") // constructor injects are not possible for test classes public abstract class AbstractRemoteEventTest extends AbstractJpaIntegrationTest { @Autowired private BusProtoStuffMessageConverter busProtoStuffMessageConverter; - private AbstractMessageConverter jacksonMessageConverter; @BeforeEach public void setup() throws Exception { final BusJacksonAutoConfiguration autoConfiguration = new BusJacksonAutoConfiguration(); this.jacksonMessageConverter = autoConfiguration.busJsonConverter(null); - ReflectionTestUtils.setField(jacksonMessageConverter, "packagesToScan", - new String[] { "org.eclipse.hawkbit.repository.event.remote", - ClassUtils.getPackageName(RemoteApplicationEvent.class) }); + ReflectionTestUtils.setField( + jacksonMessageConverter, "packagesToScan", + new String[] { "org.eclipse.hawkbit.repository.event.remote", ClassUtils.getPackageName(RemoteApplicationEvent.class) }); ((InitializingBean) jacksonMessageConverter).afterPropertiesSet(); - - } - - protected Message createMessageWithImmutableHeader(final TenantAwareEvent event) { - final Map headers = new LinkedHashMap<>(); - return busProtoStuffMessageConverter.toMessage(event, new MessageHeaders(headers)); } @SuppressWarnings("unchecked") @@ -90,4 +85,4 @@ import org.springframework.util.MimeTypeUtils; } return null; } -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteIdEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteIdEventTest.java index f6a6446ca..83b94abfa 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteIdEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteIdEventTest.java @@ -27,8 +27,8 @@ import org.junit.jupiter.api.Test; */ class RemoteIdEventTest extends AbstractRemoteEventTest { - private static final long ENTITY_ID = 1L; private static final String TENANT = "tenant"; + private static final long ENTITY_ID = 1L; private static final Class ENTITY_CLASS = JpaAction.class; private static final String CONTROLLER_ID = "controller911"; private static final String ADDRESS = "amqp://anyhost"; @@ -109,4 +109,4 @@ class RemoteIdEventTest extends AbstractRemoteEventTest { // gets added because events inherit from of java.util.EventObject assertThat(underTestCreatedEvent).usingRecursiveComparison().ignoringFields("source").isEqualTo(event); } -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteTenantAwareEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteTenantAwareEventTest.java index 11e0625b3..a6d9e36b1 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteTenantAwareEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/RemoteTenantAwareEventTest.java @@ -86,7 +86,6 @@ class RemoteTenantAwareEventTest extends AbstractRemoteEventTest { */ @Test void testTargetAssignDistributionSetEvent() { - final DistributionSet dsA = testdataFactory.createDistributionSet(""); final JpaAction generateAction = new JpaAction(); @@ -114,7 +113,6 @@ class RemoteTenantAwareEventTest extends AbstractRemoteEventTest { */ @Test void testCancelTargetAssignmentEvent() { - final DistributionSet dsA = testdataFactory.createDistributionSet(""); final JpaAction generateAction = new JpaAction(); @@ -145,9 +143,7 @@ class RemoteTenantAwareEventTest extends AbstractRemoteEventTest { return generateAction; } - private void assertTargetAssignDistributionSetEvent(final Action action, - final TargetAssignDistributionSetEvent underTest) { - + private void assertTargetAssignDistributionSetEvent(final Action action, final TargetAssignDistributionSetEvent underTest) { assertThat(underTest.getActions()).hasSize(1); final ActionProperties actionProperties = underTest.getActions().get(action.getTarget().getControllerId()); assertThat(actionProperties).isNotNull(); @@ -162,4 +158,4 @@ class RemoteTenantAwareEventTest extends AbstractRemoteEventTest { assertThat(actionProperties).usingRecursiveComparison().comparingOnlyFields().isEqualTo(new ActionProperties(action)); assertThat(underTest.getActionPropertiesForController(action.getTarget().getControllerId())).isPresent(); } -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/AbstractRemoteEntityEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/AbstractRemoteEntityEventTest.java index d85749c87..944873ab1 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/AbstractRemoteEntityEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/AbstractRemoteEntityEventTest.java @@ -70,4 +70,4 @@ public abstract class AbstractRemoteEntityEventTest extends AbstractRemoteEve } protected abstract E createEntity(); -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/ActionEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/ActionEventTest.java index 4ba233581..23935ff5d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/ActionEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/ActionEventTest.java @@ -90,5 +90,4 @@ class ActionEventTest extends AbstractRemoteEntityEventTest { generateAction.setWeight(1000); return actionRepository.save(generateAction); } - -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/DistributionSetCreatedEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/DistributionSetCreatedEventTest.java index 2ebbd38d3..5cb5f9de5 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/DistributionSetCreatedEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/DistributionSetCreatedEventTest.java @@ -30,7 +30,7 @@ class DistributionSetCreatedEventTest extends AbstractRemoteEntityEventTest { .type("os").modules(Collections.singletonList(module.getId()))); return rolloutManagement.create( - entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").distributionSetId(ds), 5, - false, new RolloutGroupConditionBuilder().withDefaults() - .successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build()); + entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").distributionSetId(ds), + 5, + false, + new RolloutGroupConditionBuilder().withDefaults().successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build()); } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/RolloutGroupEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/RolloutGroupEventTest.java index 79fe77be5..c69242760 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/RolloutGroupEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/RolloutGroupEventTest.java @@ -11,7 +11,7 @@ package org.eclipse.hawkbit.repository.event.remote.entity; import static org.assertj.core.api.Assertions.assertThat; -import java.util.Collections; +import java.util.List; import java.util.UUID; import org.eclipse.hawkbit.repository.model.DistributionSet; @@ -81,13 +81,16 @@ class RolloutGroupEventTest extends AbstractRemoteEntityEventTest final SoftwareModule module = softwareModuleManagement.create( entityFactory.softwareModule().create().name("swm").version("2").description("desc").type("os")); final DistributionSet ds = distributionSetManagement - .create(entityFactory.distributionSet().create().name("complete").version("2").description("complete") - .type("os").modules(Collections.singletonList(module.getId()))); + .create(entityFactory.distributionSet().create() + .name("complete").version("2") + .description("complete").type("os") + .modules(List.of(module.getId()))); final Rollout entity = rolloutManagement.create( - entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").distributionSetId(ds), 5, - false, new RolloutGroupConditionBuilder().withDefaults() - .successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build()); + entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").distributionSetId(ds), + 5, + false, + new RolloutGroupConditionBuilder().withDefaults().successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build()); return rolloutGroupManagement.findByRollout(entity.getId(), PAGE).getContent().get(0); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetEventTest.java index 6cdbc2343..cac963729 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetEventTest.java @@ -40,5 +40,4 @@ class TargetEventTest extends AbstractRemoteEntityEventTest { protected Target createEntity() { return testdataFactory.createTarget("12345"); } - -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetTagEventTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetTagEventTest.java index 6a9f517bd..826bec43a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetTagEventTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/event/remote/entity/TargetTagEventTest.java @@ -40,5 +40,4 @@ class TargetTagEventTest extends AbstractRemoteEntityEventTest { protected TargetTag createEntity() { return targetTagManagement.create(entityFactory.tag().create().name("tag1")); } - -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java index 0e4dc89bc..f2812856d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/AbstractJpaIntegrationTest.java @@ -10,6 +10,8 @@ package org.eclipse.hawkbit.repository.jpa; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withUser; import java.lang.reflect.Array; import java.util.Collection; @@ -62,7 +64,6 @@ import org.eclipse.hawkbit.repository.model.TargetTypeAssignmentResult; import org.eclipse.hawkbit.repository.test.TestConfiguration; import org.eclipse.hawkbit.repository.test.util.AbstractIntegrationTest; import org.eclipse.hawkbit.repository.test.util.RolloutTestApprovalStrategy; -import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch; import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; @@ -73,7 +74,7 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.transaction.annotation.Transactional; @Slf4j -@ContextConfiguration(classes = { RepositoryApplicationConfiguration.class, TestConfiguration.class }) +@ContextConfiguration(classes = { JpaRepositoryConfiguration.class, TestConfiguration.class }) @TestPropertySource(locations = "classpath:/jpa-test.properties") @SuppressWarnings("java:S6813") // constructor injects are not possible for test classes public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest { @@ -82,7 +83,9 @@ public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest protected static final String NOT_EXIST_ID = "12345678990"; protected static final long NOT_EXIST_IDL = Long.parseLong(NOT_EXIST_ID); - private static final List REPOSITORY_AND_TARGET_PERMISSIONS = List.of(SpPermission.READ_REPOSITORY, SpPermission.CREATE_REPOSITORY, SpPermission.UPDATE_REPOSITORY, SpPermission.DELETE_REPOSITORY, SpPermission.READ_TARGET, SpPermission.CREATE_TARGET, SpPermission.UPDATE_TARGET, SpPermission.DELETE_TARGET); + private static final List REPOSITORY_AND_TARGET_PERMISSIONS = List.of(SpPermission.READ_REPOSITORY, SpPermission.CREATE_REPOSITORY, + SpPermission.UPDATE_REPOSITORY, SpPermission.DELETE_REPOSITORY, SpPermission.READ_TARGET, SpPermission.CREATE_TARGET, + SpPermission.UPDATE_TARGET, SpPermission.DELETE_TARGET); @PersistenceContext protected EntityManager entityManager; @@ -230,33 +233,33 @@ public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest /** * Asserts that the given callable throws an InsufficientPermissionException. + * * @param callable the callable to call * @param requiredPermissions required permissions for the callable * @param insufficientPermissions can be null, if null, it will be resolved automatically. But in some cases (e.g. @PreAuthorized Permissions with OR, it is safer to pass directly the insufficient permissions) */ @SneakyThrows - protected void assertPermissions(final Callable callable, final List requiredPermissions, final List insufficientPermissions) { + protected void assertPermissions(final Callable callable, final List requiredPermissions, + final List insufficientPermissions) { // if READ_PERMISSION is required and required permissions are multiple, give only READ_PERMISSION to eliminate internal read_permission check failure that would confuse the actual test - final List resolvedInsufficientPermissions = insufficientPermissions != null ? insufficientPermissions : - requiredPermissions.contains(SpPermission.READ_REPOSITORY) && requiredPermissions.size() > 1 ? - List.of(SpPermission.READ_REPOSITORY) : REPOSITORY_AND_TARGET_PERMISSIONS.stream() - .filter(p -> !requiredPermissions.contains(p)).toList(); + final List resolvedInsufficientPermissions = insufficientPermissions != null + ? insufficientPermissions + : requiredPermissions.contains(SpPermission.READ_REPOSITORY) && requiredPermissions.size() > 1 + ? List.of(SpPermission.READ_REPOSITORY) + : REPOSITORY_AND_TARGET_PERMISSIONS.stream().filter(p -> !requiredPermissions.contains(p)).toList(); // check if the user has the correct permissions - SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("user_with_permissions", requiredPermissions.toArray(new String[0])), () -> { + runAs(withUser("user_with_permissions", requiredPermissions.toArray(new String[0])), () -> { assertPermissionWorks(callable); log.info("assertPermissionWorks Passed"); - return null; }); // check if the user has the insufficient permissions - SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("user_without_permissions", resolvedInsufficientPermissions.toArray(new String[0])), () -> { + runAs(withUser("user_without_permissions", resolvedInsufficientPermissions.toArray(new String[0])), () -> { assertInsufficientPermission(callable); log.info("assertInsufficientPermission Passed"); - return null; }); } - /** * Asserts that the given callable throws an InsufficientPermissionException. * If callable succeeds without any exception or exception other than InsufficientPermissionException, it will be considered as an assert failure. diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/context/ContextAwareTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ContextAwareTest.java similarity index 97% rename from hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/context/ContextAwareTest.java rename to hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ContextAwareTest.java index c24ed5be3..d2a384e98 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/context/ContextAwareTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ContextAwareTest.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2023 Bosch.IO GmbH and others + * Copyright (c) 2025 Contributors to the Eclipse Foundation * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 @@ -7,7 +7,7 @@ * * SPDX-License-Identifier: EPL-2.0 */ -package org.eclipse.hawkbit.repository.jpa.acm.context; +package org.eclipse.hawkbit.repository.jpa.acm; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/DistributionSetAccessControllerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/DistributionSetAccessControllerTest.java new file mode 100644 index 000000000..b92d9a6e9 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/DistributionSetAccessControllerTest.java @@ -0,0 +1,274 @@ +/** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.hawkbit.repository.jpa.acm; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_REPOSITORY; +import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_TARGET; +import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_REPOSITORY; +import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_TARGET; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withUser; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.hawkbit.repository.Identifiable; +import org.eclipse.hawkbit.repository.builder.AutoAssignDistributionSetUpdate; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; +import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; +import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.DistributionSetFilter; +import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.ContextConfiguration; + +/** + * Feature: Component Tests - Access Control
+ * Story: Test Distribution Set Access Controller + */ +@ContextConfiguration(classes = { DefaultAccessControllerConfiguration.class }) +class DistributionSetAccessControllerTest extends AbstractJpaIntegrationTest { + + /** + * Verifies read access rules for distribution sets + */ + @Test + void verifyDistributionSetReadOperations() { + final DistributionSet permitted = testdataFactory.createDistributionSet(); + final DistributionSet hidden = testdataFactory.createDistributionSet(); + + final Action permittedAction = testdataFactory.performAssignment(permitted); + final Action hiddenAction = testdataFactory.performAssignment(hidden); + + runAs(withUser("user", + READ_REPOSITORY + "/id==" + permitted.getId(), + READ_TARGET +"/controllerId==" + permittedAction.getTarget().getControllerId()), () -> { + final Long permittedActionId = permitted.getId(); + + // verify distributionSetManagement#findAll + assertThat(distributionSetManagement.findAll(Pageable.unpaged()).get().map(Identifiable::getId).toList()) + .containsOnly(permittedActionId); + + // verify distributionSetManagement#findByRsql + assertThat(distributionSetManagement.findByRsql("name==*", Pageable.unpaged()).get().map(Identifiable::getId) + .toList()).containsOnly(permittedActionId); + + // verify distributionSetManagement#findByCompleted + assertThat(distributionSetManagement.findByCompleted(true, Pageable.unpaged()).get().map(Identifiable::getId) + .toList()).containsOnly(permittedActionId); + + // verify distributionSetManagement#findByDistributionSetFilter + assertThat(distributionSetManagement + .findByDistributionSetFilter(DistributionSetFilter.builder().isDeleted(false).build(), Pageable.unpaged()) + .get().map(Identifiable::getId).toList()).containsOnly(permittedActionId); + + // verify distributionSetManagement#get + assertThat(distributionSetManagement.get(permittedActionId)).isPresent(); + final Long hiddenId = hidden.getId(); + assertThat(distributionSetManagement.get(hiddenId)).isEmpty(); + + // verify distributionSetManagement#getWithDetails + assertThat(distributionSetManagement.getWithDetails(permittedActionId)).isPresent(); + assertThat(distributionSetManagement.getWithDetails(hiddenId)).isEmpty(); + + // verify distributionSetManagement#get + assertThat(distributionSetManagement.getValid(permittedActionId).getId()).isEqualTo(permittedActionId); + assertThatThrownBy(() -> distributionSetManagement.getValid(hiddenId)) + .as("Distribution set should not be found.").isInstanceOf(EntityNotFoundException.class); + + // verify distributionSetManagement#get + final List allActionIds = Arrays.asList(permittedActionId, hiddenId); + assertThatThrownBy(() -> distributionSetManagement.get(allActionIds)) + .as("Fail if request hidden.").isInstanceOf(EntityNotFoundException.class); + + // verify distributionSetManagement#getByNameAndVersion + assertThat(distributionSetManagement.findByNameAndVersion(permitted.getName(), permitted.getVersion())).isPresent(); + assertThat(distributionSetManagement.findByNameAndVersion(hidden.getName(), hidden.getVersion())).isEmpty(); + + // verify distributionSetManagement#getByAction + assertThat(distributionSetManagement.findByAction(permittedAction.getId())).isPresent(); + final Long hiddenActionId = hiddenAction.getId(); + assertThatThrownBy(() -> distributionSetManagement.findByAction(hiddenActionId)) + .as("Action is hidden.").isInstanceOf(InsufficientPermissionException.class); + }); + } + + /** + * Verifies read access rules for distribution sets + */ + @Test + void verifyDistributionSetUpdates() { + final DistributionSet permitted = testdataFactory.createDistributionSet(); + final String mdPresetKey = "metadata.preset"; + final String mdPresetValue = "presetValue"; + distributionSetManagement.createMetadata(permitted.getId(), Map.of(mdPresetKey, mdPresetValue)); + final DistributionSet readOnly = testdataFactory.createDistributionSet(); + distributionSetManagement.createMetadata(readOnly.getId(), Map.of(mdPresetKey, mdPresetValue)); + final DistributionSet hidden = testdataFactory.createDistributionSet(); + distributionSetManagement.createMetadata(hidden.getId(), Map.of(mdPresetKey, mdPresetValue)); + + final SoftwareModule swModule = testdataFactory.createSoftwareModuleOs(); + + runAs(withUser("user", + READ_REPOSITORY + "/id==" + permitted.getId() + " or id==" + readOnly.getId(), + UPDATE_REPOSITORY + "/id==" + permitted.getId()), () -> { + // verify distributionSetManagement#assignSoftwareModules + final List singleModuleIdList = Collections.singletonList(swModule.getId()); + assertThat(distributionSetManagement.assignSoftwareModules(permitted.getId(), singleModuleIdList)) + .satisfies(ds -> assertThat(ds.getModules().stream().map(Identifiable::getId).toList()).contains(swModule.getId())); + final Long readOnlyId = readOnly.getId(); + assertThatThrownBy(() -> distributionSetManagement.assignSoftwareModules(readOnlyId, singleModuleIdList)) + .as("Distribution set not allowed to me modified.") + .isInstanceOf(InsufficientPermissionException.class); + final Long hiddenId = hidden.getId(); + assertThatThrownBy(() -> distributionSetManagement.assignSoftwareModules(hiddenId, singleModuleIdList)) + .as("Distribution set should not be visible.") + .isInstanceOf(EntityNotFoundException.class); + + final Map metadata = Map.of("test.create", mdPresetValue); + + // verify distributionSetManagement#createMetaData + distributionSetManagement.createMetadata(permitted.getId(), metadata); + assertThatThrownBy(() -> distributionSetManagement.createMetadata(readOnlyId, metadata)) + .as("Distribution set not allowed to be modified.") + .isInstanceOf(InsufficientPermissionException.class); + assertThatThrownBy(() -> distributionSetManagement.createMetadata(hiddenId, metadata)) + .as("Distribution set should not be visible.") + .isInstanceOf(EntityNotFoundException.class); + + // verify distributionSetManagement#updateMetaData + final String newValue = "newValue"; + distributionSetManagement.updateMetadata(permitted.getId(), mdPresetKey, newValue); + assertThatThrownBy(() -> distributionSetManagement.updateMetadata(readOnlyId, mdPresetKey, newValue)) + .as("Distribution set not allowed to me modified.") + .isInstanceOf(InsufficientPermissionException.class); + assertThatThrownBy(() -> distributionSetManagement.updateMetadata(hiddenId, mdPresetKey, newValue)) + .as("Distribution set should not be visible.") + .isInstanceOf(EntityNotFoundException.class); + + // verify distributionSetManagement#deleteMetaData + final String metadataKey = metadata.entrySet().stream().findAny().get().getKey(); + distributionSetManagement.deleteMetadata(permitted.getId(), metadataKey); + assertThatThrownBy(() -> distributionSetManagement.deleteMetadata(readOnlyId, mdPresetKey)) + .as("Distribution set not allowed to me modified.") + .isInstanceOf(InsufficientPermissionException.class); + assertThatThrownBy(() -> distributionSetManagement.deleteMetadata(hiddenId, mdPresetKey)) + .as("Distribution set should not be visible.") + .isInstanceOf(EntityNotFoundException.class); + }); + } + + @Test + void verifyTagFilteringAndManagement() { + final DistributionSet permitted = testdataFactory.createDistributionSet(); + final DistributionSet readOnly = testdataFactory.createDistributionSet(); + final DistributionSet hidden = testdataFactory.createDistributionSet(); + final Long dsTagId = distributionSetTagManagement.create(entityFactory.tag().create().name("dsTag")).getId(); + final Long dsTag2Id = distributionSetTagManagement.create(entityFactory.tag().create().name("dsTag2")).getId(); + + // perform tag assignment before setting access rules + distributionSetManagement.assignTag(Arrays.asList(permitted.getId(), readOnly.getId(), hidden.getId()), dsTagId); + + runAs(withUser("user", + READ_REPOSITORY + "/id==" + permitted.getId() + " or id==" + readOnly.getId(), + UPDATE_REPOSITORY + "/id==" + permitted.getId()), () -> { + assertThat(distributionSetManagement.findByTag(dsTagId, Pageable.unpaged()).get().map(Identifiable::getId) + .toList()).containsOnly(permitted.getId(), readOnly.getId()); + + assertThat(distributionSetManagement.findByRsqlAndTag("name==*", dsTagId, Pageable.unpaged()).get() + .map(Identifiable::getId).toList()).containsOnly(permitted.getId(), readOnly.getId()); + + // verify distributionSetManagement#unassignTag on permitted target + assertThat(distributionSetManagement + .unassignTag(Collections.singletonList(permitted.getId()), dsTagId)) + .size() + .isEqualTo(1); + // verify distributionSetManagement#assignTag on permitted target + assertThat(distributionSetManagement.assignTag(Collections.singletonList(permitted.getId()), dsTagId)) + .hasSize(1); + // verify distributionSetManagement#unAssignTag on permitted target + assertThat(distributionSetManagement.unassignTag(List.of(permitted.getId()), dsTagId) + .get(0).getId()) + .isEqualTo(permitted.getId()); + + // assignment is denied for readOnlyTarget (read, but no update permissions) + final List readOblyList = Collections.singletonList(readOnly.getId()); + assertThatThrownBy(() -> + distributionSetManagement.unassignTag(readOblyList, dsTagId)) + .as("Missing update permissions for target to toggle tag assignment.") + .isInstanceOf(InsufficientPermissionException.class); + + // assignment is denied for readOnlyTarget (read, but no update permissions) + // dsTag2- since - it is tagged with dsTag and won't do anything if assigning dsTag + assertThatThrownBy(() -> distributionSetManagement.assignTag(readOblyList, dsTag2Id)) + .as("Missing update permissions for target to toggle tag assignment.") + .isInstanceOf(InsufficientPermissionException.class); + + // assignment is denied for hiddenTarget since it's hidden + final List hiddenList = Collections.singletonList(hidden.getId()); + assertThatThrownBy(() -> distributionSetManagement.unassignTag(hiddenList, dsTagId)) + .as("Missing update permissions for target to toggle tag assignment.") + .isInstanceOf(EntityNotFoundException.class); + + // assignment is denied for hiddenTarget since it's hidden + assertThatThrownBy(() -> distributionSetManagement.assignTag(hiddenList, dsTagId)) + .as("Missing update permissions for target to toggle tag assignment.") + .isInstanceOf(EntityNotFoundException.class); + + // assignment is denied for hiddenTarget since it's hidden + final List hiddenIdList = List.of(hidden.getId()); + assertThatThrownBy(() -> distributionSetManagement.unassignTag(hiddenIdList, dsTagId)) + .as("Missing update permissions for target to toggle tag assignment.") + .isInstanceOf(EntityNotFoundException.class); + }); + } + + @Test + void verifyAutoAssignmentUsage() { + final DistributionSet permitted = testdataFactory.createDistributionSet(); + final DistributionSet readOnly = testdataFactory.createDistributionSet(); + final DistributionSet hidden = testdataFactory.createDistributionSet(); + // has to lock them, otherwise implicit lock shall be made which require DistributionSet update permissions + distributionSetManagement.lock(permitted.getId()); + distributionSetManagement.lock(readOnly.getId()); + distributionSetManagement.lock(hidden.getId()); + + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement + .create(entityFactory.targetFilterQuery().create().name("test").query("id==*")); + + runAs(withUser("user", + READ_REPOSITORY + "/id==" + permitted.getId() + " or id==" + readOnly.getId(), + UPDATE_REPOSITORY + "/id==" + permitted.getId(), + // read / update target needed to update target filter query + READ_TARGET, UPDATE_TARGET), () -> { + assertThat(targetFilterQueryManagement + .updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()).ds(permitted.getId()) + .actionType(Action.ActionType.FORCED).confirmationRequired(false)) + .getAutoAssignDistributionSet().getId()).isEqualTo(permitted.getId()); + targetFilterQueryManagement + .updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()) + .ds(readOnly.getId()).actionType(Action.ActionType.FORCED).confirmationRequired(false)) + .getAutoAssignDistributionSet().getId(); + final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate = new AutoAssignDistributionSetUpdate( + targetFilterQuery.getId()) + .ds(hidden.getId()).actionType(Action.ActionType.FORCED).confirmationRequired(false); + assertThatThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(autoAssignDistributionSetUpdate)) + .isInstanceOf(EntityNotFoundException.class); + }); + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetAccessControllerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetAccessControllerTest.java new file mode 100644 index 000000000..76584428d --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetAccessControllerTest.java @@ -0,0 +1,340 @@ +/** + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.hawkbit.repository.jpa.acm; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.hawkbit.im.authentication.SpPermission.CREATE_ROLLOUT; +import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_REPOSITORY; +import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_ROLLOUT; +import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_TARGET; +import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_TARGET; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withUser; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.eclipse.hawkbit.repository.FilterParams; +import org.eclipse.hawkbit.repository.Identifiable; +import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; +import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; +import org.eclipse.hawkbit.repository.jpa.autoassign.AutoAssignChecker; +import org.eclipse.hawkbit.repository.model.Action.ActionType; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.Rollout; +import org.eclipse.hawkbit.repository.model.RolloutGroup; +import org.eclipse.hawkbit.repository.model.Target; +import org.eclipse.hawkbit.repository.model.TargetFilterQuery; +import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.ContextConfiguration; + +/** + * Feature: Component Tests - Access Control
+ * Story: Test Target Access Controller + */ +@ContextConfiguration(classes = { DefaultAccessControllerConfiguration.class }) +class TargetAccessControllerTest extends AbstractJpaIntegrationTest { + + @Autowired + AutoAssignChecker autoAssignChecker; + + /** + * Verifies read access rules for targets + */ + @Test + void verifyTargetReadOperations() { + final Target permittedTarget = targetManagement + .create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED)); + + final Target hiddenTarget = targetManagement + .create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED)); + + runAs(withUser("user", READ_TARGET + "/controllerId==" + permittedTarget.getControllerId()), () -> { + // verify targetManagement#findAll + assertThat(targetManagement.findAll(Pageable.unpaged()).get().map(Identifiable::getId).toList()) + .containsOnly(permittedTarget.getId()); + + // verify targetManagement#findByRsql + assertThat(targetManagement.findByRsql("id==*", Pageable.unpaged()).get().map(Identifiable::getId).toList()) + .containsOnly(permittedTarget.getId()); + + // verify targetManagement#findByUpdateStatus + assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.REGISTERED, Pageable.unpaged()).get() + .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()); + + // verify targetManagement#getByControllerID + assertThat(targetManagement.getByControllerID(permittedTarget.getControllerId())).isPresent(); + final String hiddenTargetControllerId = hiddenTarget.getControllerId(); + assertThatThrownBy(() -> targetManagement.getByControllerID(hiddenTargetControllerId)) + .as("Missing read permissions for hidden target.") + .isInstanceOf(InsufficientPermissionException.class); + + // verify targetManagement#getByControllerID + assertThat(targetManagement + .getByControllerID(Arrays.asList(permittedTarget.getControllerId(), hiddenTargetControllerId)) + .stream().map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()); + + // verify targetManagement#get + assertThat(targetManagement.get(permittedTarget.getId())).isPresent(); + assertThat(targetManagement.get(hiddenTarget.getId())).isEmpty(); + + // verify targetManagement#get + assertThat(targetManagement.get(Arrays.asList(permittedTarget.getId(), hiddenTarget.getId())).stream() + .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()); + + // verify targetManagement#getControllerAttributes + assertThat(targetManagement.getControllerAttributes(permittedTarget.getControllerId())).isEmpty(); + assertThatThrownBy(() -> targetManagement.getControllerAttributes(hiddenTargetControllerId)) + .as("Target should not be found.") + .isInstanceOf(InsufficientPermissionException.class); + }); + + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement + .create(entityFactory.targetFilterQuery().create().name("test").query("id==*")); + + runAs(withUser("user", READ_TARGET + "/controllerId==" + permittedTarget.getControllerId()), () -> { + // verify targetManagement#findByTargetFilterQuery + assertThat(targetManagement.findByTargetFilterQuery(targetFilterQuery.getId(), Pageable.unpaged()).get() + .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()); + + // verify targetManagement#findByTargetFilterQuery (used by UI) + assertThat(targetManagement.findByFilters(new FilterParams(null, null, null, null), Pageable.unpaged()).get() + .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()); + }); + } + + @Test + void verifyTagFilteringAndManagement() { + final Target permittedTarget = targetManagement + .create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED)); + + final Target readOnlyTarget = targetManagement + .create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED)); + final String readOnlyTargetControllerId = readOnlyTarget.getControllerId(); + + final Target hiddenTarget = targetManagement + .create(entityFactory.target().create().controllerId("device03").status(TargetUpdateStatus.REGISTERED)); + + final Long myTagId = targetTagManagement.create(entityFactory.tag().create().name("myTag")).getId(); + + // perform tag assignment before setting access rules + targetManagement.assignTag( + Arrays.asList(permittedTarget.getControllerId(), readOnlyTargetControllerId, hiddenTarget.getControllerId()), myTagId); + + runAs(withUser("user", + READ_TARGET + "/controllerId==" + permittedTarget.getControllerId() + " or controllerId==" + readOnlyTargetControllerId, + UPDATE_TARGET + "/controllerId==" + permittedTarget.getControllerId(), + READ_REPOSITORY), + () -> { + // verify targetManagement#findByTag + assertThat( + targetManagement.findByTag(myTagId, Pageable.unpaged()).get().map(Identifiable::getId).toList()) + .containsOnly(permittedTarget.getId(), readOnlyTarget.getId()); + + // verify targetManagement#findByRsqlAndTag + assertThat(targetManagement.findByRsqlAndTag("id==*", myTagId, Pageable.unpaged()).get() + .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId(), readOnlyTarget.getId()); + + // verify targetManagement#assignTag on permitted target + assertThat(targetManagement.assignTag(Collections.singletonList(permittedTarget.getControllerId()), myTagId)).hasSize(1); + // verify targetManagement#unassignTag on permitted target + assertThat(targetManagement.unassignTag(Collections.singletonList(permittedTarget.getControllerId()), myTagId)).hasSize(1); + // verify targetManagement#assignTag on permitted target + assertThat(targetManagement.assignTag(Collections.singletonList(permittedTarget.getControllerId()), myTagId)) + .hasSize(1); + // verify targetManagement#unAssignTag on permitted target + assertThat(targetManagement.unassignTag(List.of(permittedTarget.getControllerId()), myTagId).get(0).getControllerId()) + .isEqualTo(permittedTarget.getControllerId()); + + // assignment is denied for readOnlyTarget (read, but no update permissions) + final List readTargetControllerIdList = Collections.singletonList(readOnlyTargetControllerId); + assertThatThrownBy(() -> targetManagement.assignTag(readTargetControllerIdList, myTagId)) + .as("Missing update permissions for target to toggle tag assignment.") + .isInstanceOfAny(InsufficientPermissionException.class); + + // assignment is denied for readOnlyTarget (read, but no update permissions) + final List readOnlyTargetControllerIdList = List.of(readOnlyTargetControllerId); + assertThatThrownBy(() -> targetManagement.unassignTag(readOnlyTargetControllerIdList, myTagId)) + .as("Missing update permissions for target to toggle tag assignment.") + .isInstanceOf(InsufficientPermissionException.class); + + // assignment is denied for hiddenTarget since it's hidden + final List hiddenTargetControllerIdList = Collections.singletonList(hiddenTarget.getControllerId()); + assertThatThrownBy(() -> targetManagement.assignTag(hiddenTargetControllerIdList, myTagId)) + .as("Missing update permissions for target to toggle tag assignment.") + .isInstanceOf(InsufficientPermissionException.class); + + // assignment is denied for hiddenTarget since it's hidden + assertThatThrownBy(() -> targetManagement.assignTag(hiddenTargetControllerIdList, myTagId)) + .as("Missing update permissions for target to toggle tag assignment.") + .isInstanceOf(InsufficientPermissionException.class); + + // assignment is denied for hiddenTarget since it's hidden + assertThatThrownBy(() -> targetManagement.unassignTag(hiddenTargetControllerIdList, myTagId)) + .as("Missing update permissions for target to toggle tag assignment.") + .isInstanceOf(InsufficientPermissionException.class); + }); + } + + /** + * Verifies rules for target assignment + */ + @Test + void verifyTargetAssignment() { + final Long dsId = testdataFactory.createDistributionSet("myDs").getId(); + distributionSetManagement.lock(dsId); + + final Target permittedTarget = targetManagement + .create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED)); + final String hiddenTargetControllerId = targetManagement + .create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED)) + .getControllerId(); + + runAs(withUser("user", READ_TARGET + "/controllerId==" + permittedTarget.getControllerId()), () -> + // verify targetManagement#findByUpdateStatus before assignment + assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.REGISTERED, Pageable.unpaged()).get() + .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId())); + + runAs(withUser("user", + READ_TARGET + "/controllerId==" + permittedTarget.getControllerId(), + UPDATE_TARGET + "/controllerId==" + permittedTarget.getControllerId(), + READ_REPOSITORY), () -> { + assertThat(assignDistributionSet(dsId, permittedTarget.getControllerId()).getAssigned()).isEqualTo(1); + // assigning of not allowed target behaves as not found + assertThatThrownBy(() -> assignDistributionSet(dsId, hiddenTargetControllerId)).isInstanceOf(AssertionError.class); + + // verify targetManagement#findByUpdateStatus(REGISTERED) after assignment + assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.REGISTERED, Pageable.unpaged()) + .getTotalElements()).isZero(); + + // verify targetManagement#findByUpdateStatus(PENDING) after assignment + assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.PENDING, Pageable.unpaged()).get() + .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()); + }); + } + + /** + * Verifies rules for target assignment + */ + @Test + void verifyTargetAssignmentOnNonUpdatableTarget() { + final Long firstDsId = testdataFactory.createDistributionSet("myDs").getId(); + distributionSetManagement.lock(firstDsId); + final DistributionSet secondDs = testdataFactory.createDistributionSet("anotherDs"); + distributionSetManagement.lock(secondDs.getId()); + + final Target manageableTarget = targetManagement + .create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED)); + final Target readOnlyTarget = targetManagement + .create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED)); + + runAs(withUser("user", + READ_TARGET + "/controllerId==" + manageableTarget.getControllerId() + " or controllerId==" + readOnlyTarget.getControllerId(), + UPDATE_TARGET + "/controllerId==" + manageableTarget.getControllerId(), + READ_REPOSITORY), () -> { + // assignment is permitted for manageableTarget + assertThat(assignDistributionSet(firstDsId, manageableTarget.getControllerId()).getAssigned()).isEqualTo(1); + + // assignment is denied for readOnlyTarget (read, but no update permissions) + final var readOnlyTargetControllerId = readOnlyTarget.getControllerId(); + assertThatThrownBy(() -> assignDistributionSet(firstDsId, readOnlyTargetControllerId)).isInstanceOf(AssertionError.class); + + // bunch assignment skips denied since at least one target without update permissions is present + assertThat(assignDistributionSet(secondDs.getId(), + Arrays.asList(readOnlyTargetControllerId, manageableTarget.getControllerId()), + ActionType.FORCED).getAssigned()).isEqualTo(1); + }); + } + + /** + * Verifies only manageable targets are part of the rollout + */ + @Test + void verifyRolloutTargetScope() { + final DistributionSet ds = testdataFactory.createDistributionSet("myDs"); + distributionSetManagement.lock(ds.getId()); + + final String[] updateTargetControllerIds = { "update1", "update2", "update3" }; + final List updateTargets = testdataFactory.createTargets(updateTargetControllerIds); + final String[] readTargetControllerIds = { "read1", "read2", "read3", "read4" }; + final List readTargets = testdataFactory.createTargets(readTargetControllerIds); + final List hiddenTargets = testdataFactory.createTargets("hidden1", "hidden2", "hidden3", "hidden4", "hidden5"); + + runAs(withUser("user", + READ_TARGET + "/controllerId=in=(" + String.join(", ", List.of(updateTargetControllerIds)) + ")" + + " or controllerId=in=(" + String.join(", ", List.of(readTargetControllerIds)) + ")", + UPDATE_TARGET + "/controllerId=in=(" + String.join(", ", List.of(updateTargetControllerIds)) + ")", + READ_REPOSITORY, + CREATE_ROLLOUT, READ_ROLLOUT), () -> { + final Rollout rollout = testdataFactory.createRolloutByVariables( + "testRollout", "description", updateTargets.size(), "id==*", ds, "50", "5"); + assertThat(rollout.getTotalTargets()).isEqualTo(updateTargets.size()); + + final List content = rolloutGroupManagement.findByRollout(rollout.getId(), Pageable.unpaged()).getContent(); + assertThat(content).hasSize(updateTargets.size()); + + final List rolloutTargets = content.stream().flatMap( + group -> rolloutGroupManagement.findTargetsOfRolloutGroup(group.getId(), Pageable.unpaged()).get()) + .toList(); + + assertThat(rolloutTargets).hasSize(updateTargets.size()).allMatch( + target -> updateTargets.stream().anyMatch(readTarget -> readTarget.getId().equals(target.getId()))) + .noneMatch(target -> readTargets.stream() + .anyMatch(readTarget -> readTarget.getId().equals(target.getId()))) + .noneMatch(target -> hiddenTargets.stream() + .anyMatch(readTarget -> readTarget.getId().equals(target.getId()))); + }); + } + + /** + * Verifies only manageable targets are part of an auto assignment. + */ + @Test + void verifyAutoAssignmentTargetScope() { + final DistributionSet distributionSet = testdataFactory.createDistributionSet(); + distributionSetManagement.lock(distributionSet.getId()); + + final String[] updateTargetControllerIds = { "update1", "update2", "update3" }; + final List updateTargets = testdataFactory.createTargets(updateTargetControllerIds); + final String[] readTargetControllerIds = { "read1", "read2", "read3", "read4" }; + final List readTargets = testdataFactory.createTargets(readTargetControllerIds); + final List hiddenTargets = testdataFactory.createTargets("hidden1", "hidden2", "hidden3", "hidden4", "hidden5"); + + final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement + .create(entityFactory.targetFilterQuery().create().name("testName").query("id==*")); + + runAs(withUser("user", + READ_TARGET + "/controllerId=in=(" + String.join(", ", List.of(updateTargetControllerIds)) + ")" + + " or controllerId=in=(" + String.join(", ", List.of(readTargetControllerIds)) + ")", + UPDATE_TARGET + "/controllerId=in=(" + String.join(", ", List.of(updateTargetControllerIds)) + ")", + READ_REPOSITORY + "/id==" + distributionSet.getId()), () -> { + + targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() + .updateAutoAssign(targetFilterQuery.getId()).ds(distributionSet.getId())); + + autoAssignChecker.checkAllTargets(); + + assertThat(targetManagement.findByAssignedDistributionSet(distributionSet.getId(), Pageable.unpaged()) + .getContent()) + .hasSize(updateTargets.size()) + .allMatch(assignedTarget -> updateTargets.stream() + .anyMatch(updateTarget -> updateTarget.getId().equals(assignedTarget.getId()))) + .noneMatch(assignedTarget -> readTargets.stream() + .anyMatch(updateTarget -> updateTarget.getId().equals(assignedTarget.getId()))) + .noneMatch(assignedTarget -> hiddenTargets.stream() + .anyMatch(updateTarget -> updateTarget.getId().equals(assignedTarget.getId()))); + }); + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetTypeAccessControllerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetTypeAccessControllerTest.java new file mode 100644 index 000000000..deafc71f0 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetTypeAccessControllerTest.java @@ -0,0 +1,162 @@ +/** + * Copyright (c) 2023 Bosch.IO GmbH and others + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.hawkbit.repository.jpa.acm; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.hawkbit.im.authentication.SpPermission.DELETE_TARGET; +import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_TARGET; +import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_TARGET; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withUser; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.hawkbit.repository.Identifiable; +import org.eclipse.hawkbit.repository.builder.TargetTypeCreate; +import org.eclipse.hawkbit.repository.builder.TargetTypeUpdate; +import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; +import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; +import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; +import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; +import org.eclipse.hawkbit.repository.jpa.specifications.TargetTypeSpecification; +import org.eclipse.hawkbit.repository.model.TargetType; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.ContextConfiguration; + +/** + * Feature: Component Tests - Access Control
+ * Story: Test Target Type Access Controller + */ +@ContextConfiguration(classes = { DefaultAccessControllerConfiguration.class }) +class TargetTypeAccessControllerTest extends AbstractJpaIntegrationTest { + + /** + * Verifies read access rules for target types + */ + @Test + void verifyTargetTypeReadOperations() { + final TargetType permittedTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type1")); + final TargetType hiddenTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type2")); + + runAs(withUser("user", READ_TARGET + "/id==" + permittedTargetType.getId()), () -> { + // verify targetTypeManagement#findAll + assertThat(targetTypeManagement.findAll(Pageable.unpaged()).get().map(Identifiable::getId).toList()) + .containsOnly(permittedTargetType.getId()); + + // verify targetTypeManagement#findByRsql + assertThat(targetTypeManagement.findByRsql("name==*", Pageable.unpaged()).get().map(Identifiable::getId).toList()) + .containsOnly(permittedTargetType.getId()); + + // verify targetTypeManagement#findByName + assertThat(targetTypeManagement.findByName(permittedTargetType.getName(), Pageable.unpaged()).getContent()) + .hasSize(1).satisfies(results -> + assertThat(results.get(0).getId()).isEqualTo(permittedTargetType.getId())); + assertThat(targetTypeManagement.findByName(hiddenTargetType.getName(), Pageable.unpaged())).isEmpty(); + + // verify targetTypeManagement#count + assertThat(targetTypeManagement.count()).isEqualTo(1); + + // verify targetTypeManagement#countByName + assertThat(targetTypeManagement.countByName(permittedTargetType.getName())).isEqualTo(1); + assertThat(targetTypeManagement.countByName(hiddenTargetType.getName())).isZero(); + + // verify targetTypeManagement#countByName + assertThat(targetTypeManagement.countByName(permittedTargetType.getName())).isEqualTo(1); + assertThat(targetTypeManagement.countByName(hiddenTargetType.getName())).isZero(); + + // verify targetTypeManagement#get by id + assertThat(targetTypeManagement.get(permittedTargetType.getId())).isPresent(); + final Long hiddenTargetTypeId = hiddenTargetType.getId(); + assertThat(targetTypeManagement.get(hiddenTargetTypeId)).isEmpty(); + + // verify targetTypeManagement#getByName + assertThat(targetTypeManagement.getByName(permittedTargetType.getName())).isPresent(); + assertThat(targetTypeManagement.getByName(hiddenTargetType.getName())).isEmpty(); + + // verify targetTypeManagement#get by ids + assertThat(targetTypeManagement.get(Arrays.asList(permittedTargetType.getId(), hiddenTargetTypeId)) + .stream().map(Identifiable::getId).toList()).containsOnly(permittedTargetType.getId()); + + // verify targetTypeManagement#update is not possible. Assert exception thrown. + final TargetTypeUpdate targetTypeUpdate = entityFactory.targetType().update(hiddenTargetTypeId) + .name(hiddenTargetType.getName() + "/new").description("newDesc"); + assertThatThrownBy(() -> targetTypeManagement.update(targetTypeUpdate)) + .as("Target type update shouldn't be allowed since the target type is not visible.") + .isInstanceOf(InsufficientPermissionException.class); + + // verify targetTypeManagement#delete is not possible. Assert exception thrown. + assertThatThrownBy(() -> targetTypeManagement.delete(hiddenTargetTypeId)) + .as("Target type delete shouldn't be allowed since the target type is not visible.") + .isInstanceOf(InsufficientPermissionException.class); + }); + } + + /** + * Verifies delete access rules for target types + */ + @Test + void verifyTargetTypeDeleteOperations() { + final TargetType manageableTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type1")); + final TargetType readOnlyTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type2")); + + runAs(withUser("user", + READ_TARGET + "/id==" + manageableTargetType.getId() + " or id==" + readOnlyTargetType.getId(), + DELETE_TARGET + "/id==" + manageableTargetType.getId()), () -> { + // delete the manageableTargetType + targetTypeManagement.delete(manageableTargetType.getId()); + + // verify targetTypeManagement#delete for readOnlyTargetType is not possible + final Long readOnlyTargetTypeId = readOnlyTargetType.getId(); + assertThatThrownBy(() -> targetTypeManagement.delete(readOnlyTargetTypeId)) + .isInstanceOfAny(InsufficientPermissionException.class, EntityNotFoundException.class); + }); + } + + /** + * Verifies update operation for target types + */ + @Test + void verifyTargetTypeUpdateOperations() { + final TargetType manageableTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type1")); + final TargetType readOnlyTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type2")); + + runAs(withUser("user", + READ_TARGET + "/id==" + manageableTargetType.getId() + " or id==" + readOnlyTargetType.getId(), + UPDATE_TARGET + "/id==" + manageableTargetType.getId()), () -> { + // update the manageableTargetType + targetTypeManagement.update(entityFactory.targetType().update(manageableTargetType.getId()) + .name(manageableTargetType.getName() + "/new").description("newDesc")); + + // verify targetTypeManagement#update for readOnlyTargetType is not possible + final TargetTypeUpdate targetTypeUpdate = entityFactory.targetType().update(readOnlyTargetType.getId()) + .name(readOnlyTargetType.getName() + "/new").description("newDesc"); + assertThatThrownBy(() -> targetTypeManagement.update(targetTypeUpdate)) + .isInstanceOf(InsufficientPermissionException.class); + }); + } + + /** + * Verifies create operation blocked by controller + */ + @Test + void verifyTargetTypeCreationBlockedByAccessController() { + runAs(withUser("user", READ_TARGET, UPDATE_TARGET), () -> { + // verify targetTypeManagement#create for any type + final TargetTypeCreate targetTypeCreate = entityFactory.targetType().create().name("type1"); + assertThatThrownBy(() -> targetTypeManagement.create(targetTypeCreate)) + .as("Target type create shouldn't be allowed since the target type is not visible.") + .isInstanceOf(InsufficientPermissionException.class); + }); + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/AbstractAccessControllerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/AbstractAccessControllerTest.java deleted file mode 100644 index d6e49829b..000000000 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/AbstractAccessControllerTest.java +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Copyright (c) 2023 Bosch.IO GmbH and others - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.hawkbit.repository.jpa.acm.controller; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -import org.eclipse.hawkbit.ContextAware; -import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; -import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; -import org.eclipse.hawkbit.repository.jpa.acm.AccessController; -import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; -import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; -import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; -import org.eclipse.hawkbit.security.SecurityContextTenantAware; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.test.context.ContextConfiguration; - -@ContextConfiguration(classes = { AbstractAccessControllerTest.AccessControlTestConfig.class }) -public abstract class AbstractAccessControllerTest extends AbstractJpaIntegrationTest { - - @Autowired - protected TestAccessControlManger testAccessControlManger; - - protected static List merge(final List lists0, final List list1) { - final List merge = new ArrayList<>(lists0); - merge.addAll(list1); - return merge; - } - - protected void permitAllOperations(final AccessController.Operation operation) { - testAccessControlManger.defineAccessRule(JpaTarget.class, operation, Specification.where(null), type -> true); - testAccessControlManger.defineAccessRule(JpaTargetType.class, operation, Specification.where(null), type -> true); - testAccessControlManger.defineAccessRule(JpaDistributionSet.class, operation, Specification.where(null), type -> true); - } - - @BeforeEach - void beforeEach() { - testAccessControlManger.deleteAllRules(); - } - - @AfterEach - void afterEach() { - testAccessControlManger.deleteAllRules(); - } - - public static class AccessControlTestConfig { - - private final ContextAware contextAware = new SecurityContextTenantAware((tenant, username) -> List.of()); - - @Bean - public ContextAware contextAware() { - return contextAware; - } - - @Bean - public TestAccessControlManger accessControlTestManger() { - return new TestAccessControlManger(); - } - - @Bean - public AccessController targetAccessController(final TestAccessControlManger testAccessControlManger) { - return new AccessController<>() { - - @Override - public Optional> getAccessRules(final Operation operation) { - if (contextAware.getCurrentTenant() != null - && SecurityContextTenantAware.SYSTEM_USER.equals(contextAware.getCurrentUsername())) { - // as tenant, no restrictions - return Optional.empty(); - } - - return Optional.ofNullable(testAccessControlManger.getAccessRule(JpaTarget.class, operation)); - } - - @Override - public void assertOperationAllowed(final Operation operation, final JpaTarget entity) throws InsufficientPermissionException { - testAccessControlManger.assertOperation(JpaTarget.class, operation, List.of(entity)); - } - - @Override - public String toString() { - return AccessController.class.getSimpleName() + '<' + JpaTarget.class.getSimpleName() + '>'; - } - }; - } - - @Bean - public AccessController targetTypeAccessController(final TestAccessControlManger testAccessControlManger) { - return new AccessController<>() { - - @Override - public Optional> getAccessRules(final Operation operation) { - if (contextAware.getCurrentTenant() != null - && SecurityContextTenantAware.SYSTEM_USER.equals(contextAware.getCurrentUsername())) { - // as tenant, no restrictions - return Optional.empty(); - } - - return Optional.ofNullable(testAccessControlManger.getAccessRule(JpaTargetType.class, operation)); - } - - @Override - public void assertOperationAllowed(final Operation operation, final JpaTargetType entity) - throws InsufficientPermissionException { - testAccessControlManger.assertOperation(JpaTargetType.class, operation, List.of(entity)); - } - - @Override - public String toString() { - return AccessController.class.getSimpleName() + '<' + JpaTargetType.class.getSimpleName() + '>'; - } - }; - } - - @Bean - public AccessController distributionSetAccessController(final TestAccessControlManger testAccessControlManger) { - return new AccessController<>() { - - @Override - public Optional> getAccessRules(final Operation operation) { - if (contextAware.getCurrentTenant() != null - && SecurityContextTenantAware.SYSTEM_USER.equals(contextAware.getCurrentUsername())) { - // as tenant, no restrictions - return Optional.empty(); - } - - return Optional.ofNullable(testAccessControlManger.getAccessRule(JpaDistributionSet.class, operation)); - } - - @Override - public void assertOperationAllowed(final Operation operation, final JpaDistributionSet entity) - throws InsufficientPermissionException { - testAccessControlManger.assertOperation(JpaDistributionSet.class, operation, List.of(entity)); - } - - @Override - public String toString() { - return AccessController.class.getSimpleName() + '<' + JpaDistributionSet.class.getSimpleName() + '>'; - } - }; - } - } -} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/DistributionSetAccessControllerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/DistributionSetAccessControllerTest.java deleted file mode 100644 index 3a470520e..000000000 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/DistributionSetAccessControllerTest.java +++ /dev/null @@ -1,327 +0,0 @@ -/** - * Copyright (c) 2023 Bosch.IO GmbH and others - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.hawkbit.repository.jpa.acm.controller; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import jakarta.persistence.criteria.Predicate; - -import org.eclipse.hawkbit.repository.Identifiable; -import org.eclipse.hawkbit.repository.builder.AutoAssignDistributionSetUpdate; -import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; -import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; -import org.eclipse.hawkbit.repository.jpa.acm.AccessController; -import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; -import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet_; -import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; -import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications; -import org.eclipse.hawkbit.repository.model.Action; -import org.eclipse.hawkbit.repository.model.DistributionSet; -import org.eclipse.hawkbit.repository.model.DistributionSetFilter; -import org.eclipse.hawkbit.repository.model.SoftwareModule; -import org.eclipse.hawkbit.repository.model.TargetFilterQuery; -import org.junit.jupiter.api.Test; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; - -/** - * Feature: Component Tests - Access Control
- * Story: Test Distribution Set Access Controller - */ -class DistributionSetAccessControllerTest extends AbstractAccessControllerTest { - - /** - * Verifies read access rules for distribution sets - */ - @Test - void verifyDistributionSetReadOperations() { - permitAllOperations(AccessController.Operation.READ); - permitAllOperations(AccessController.Operation.CREATE); - permitAllOperations(AccessController.Operation.UPDATE); - - final DistributionSet permitted = testdataFactory.createDistributionSet(); - final DistributionSet hidden = testdataFactory.createDistributionSet(); - - final Action permittedAction = testdataFactory.performAssignment(permitted); - final Action hiddenAction = testdataFactory.performAssignment(hidden); - - testAccessControlManger.deleteAllRules(); - - // define access controlling rule - defineAccess(AccessController.Operation.READ, permitted); - testAccessControlManger.defineAccessRule( - JpaTarget.class, AccessController.Operation.READ, - TargetSpecifications.hasId(permittedAction.getTarget().getId()), - target -> target.getId().equals(permittedAction.getTarget().getId())); - - final Long permittedActionId = permitted.getId(); - - // verify distributionSetManagement#findAll - assertThat(distributionSetManagement.findAll(Pageable.unpaged()).get().map(Identifiable::getId).toList()) - .containsOnly(permittedActionId); - - // verify distributionSetManagement#findByRsql - assertThat(distributionSetManagement.findByRsql("name==*", Pageable.unpaged()).get().map(Identifiable::getId) - .toList()).containsOnly(permittedActionId); - - // verify distributionSetManagement#findByCompleted - assertThat(distributionSetManagement.findByCompleted(true, Pageable.unpaged()).get().map(Identifiable::getId) - .toList()).containsOnly(permittedActionId); - - // verify distributionSetManagement#findByDistributionSetFilter - assertThat(distributionSetManagement - .findByDistributionSetFilter(DistributionSetFilter.builder().isDeleted(false).build(), Pageable.unpaged()) - .get().map(Identifiable::getId).toList()).containsOnly(permittedActionId); - - // verify distributionSetManagement#get - assertThat(distributionSetManagement.get(permittedActionId)).isPresent(); - final Long hiddenId = hidden.getId(); - assertThat(distributionSetManagement.get(hiddenId)).isEmpty(); - - // verify distributionSetManagement#getWithDetails - assertThat(distributionSetManagement.getWithDetails(permittedActionId)).isPresent(); - assertThat(distributionSetManagement.getWithDetails(hiddenId)).isEmpty(); - - // verify distributionSetManagement#get - assertThat(distributionSetManagement.getValid(permittedActionId).getId()).isEqualTo(permittedActionId); - assertThatThrownBy(() -> distributionSetManagement.getValid(hiddenId)) - .as("Distribution set should not be found.").isInstanceOf(EntityNotFoundException.class); - - // verify distributionSetManagement#get - final List allActionIds = Arrays.asList(permittedActionId, hiddenId); - assertThatThrownBy(() -> distributionSetManagement.get(allActionIds)) - .as("Fail if request hidden.").isInstanceOf(EntityNotFoundException.class); - - // verify distributionSetManagement#getByNameAndVersion - assertThat(distributionSetManagement.findByNameAndVersion(permitted.getName(), permitted.getVersion())).isPresent(); - assertThat(distributionSetManagement.findByNameAndVersion(hidden.getName(), hidden.getVersion())).isEmpty(); - - // verify distributionSetManagement#getByAction - assertThat(distributionSetManagement.findByAction(permittedAction.getId())).isPresent(); - final Long hiddenActionId = hiddenAction.getId(); - assertThatThrownBy(() -> distributionSetManagement.findByAction(hiddenActionId)) - .as("Action is hidden.").isInstanceOf(InsufficientPermissionException.class); - } - - /** - * Verifies read access rules for distribution sets - */ - @Test - void verifyDistributionSetUpdates() { - // permit all operations first to prepare test setup - permitAllOperations(AccessController.Operation.READ); - permitAllOperations(AccessController.Operation.CREATE); - permitAllOperations(AccessController.Operation.UPDATE); - - final DistributionSet permitted = testdataFactory.createDistributionSet(); - final String mdPresetKey = "metadata.preset"; - final String mdPresetValue = "presetValue"; - distributionSetManagement.createMetadata(permitted.getId(), Map.of(mdPresetKey, mdPresetValue)); - final DistributionSet readOnly = testdataFactory.createDistributionSet(); - distributionSetManagement.createMetadata(readOnly.getId(), Map.of(mdPresetKey, mdPresetValue)); - final DistributionSet hidden = testdataFactory.createDistributionSet(); - distributionSetManagement.createMetadata(hidden.getId(), Map.of(mdPresetKey, mdPresetValue)); - - final SoftwareModule swModule = testdataFactory.createSoftwareModuleOs(); - - // entities created - reset rules - testAccessControlManger.deleteAllRules(); - // define access controlling rule - defineAccess(AccessController.Operation.READ, permitted, readOnly); - defineAccess(AccessController.Operation.UPDATE, permitted); - - // verify distributionSetManagement#assignSoftwareModules - final List singleModuleIdList = Collections.singletonList(swModule.getId()); - assertThat(distributionSetManagement.assignSoftwareModules(permitted.getId(), singleModuleIdList)) - .satisfies(ds -> assertThat(ds.getModules().stream().map(Identifiable::getId).toList()).contains(swModule.getId())); - final Long readOnlyId = readOnly.getId(); - assertThatThrownBy(() -> distributionSetManagement.assignSoftwareModules(readOnlyId, singleModuleIdList)) - .as("Distribution set not allowed to me modified.") - .isInstanceOf(InsufficientPermissionException.class); - final Long hiddenId = hidden.getId(); - assertThatThrownBy(() -> distributionSetManagement.assignSoftwareModules(hiddenId, singleModuleIdList)) - .as("Distribution set should not be visible.") - .isInstanceOf(EntityNotFoundException.class); - - final Map metadata = Map.of("test.create", mdPresetValue); - - // verify distributionSetManagement#createMetaData - distributionSetManagement.createMetadata(permitted.getId(), metadata); - assertThatThrownBy(() -> distributionSetManagement.createMetadata(readOnlyId, metadata)) - .as("Distribution set not allowed to be modified.") - .isInstanceOf(InsufficientPermissionException.class); - assertThatThrownBy(() -> distributionSetManagement.createMetadata(hiddenId, metadata)) - .as("Distribution set should not be visible.") - .isInstanceOf(EntityNotFoundException.class); - - // verify distributionSetManagement#updateMetaData - final String newValue = "newValue"; - distributionSetManagement.updateMetadata(permitted.getId(), mdPresetKey, newValue); - assertThatThrownBy(() -> distributionSetManagement.updateMetadata(readOnlyId, mdPresetKey, newValue)) - .as("Distribution set not allowed to me modified.") - .isInstanceOf(InsufficientPermissionException.class); - assertThatThrownBy(() -> distributionSetManagement.updateMetadata(hiddenId, mdPresetKey, newValue)) - .as("Distribution set should not be visible.") - .isInstanceOf(EntityNotFoundException.class); - - // verify distributionSetManagement#deleteMetaData - final String metadataKey = metadata.entrySet().stream().findAny().get().getKey(); - distributionSetManagement.deleteMetadata(permitted.getId(), metadataKey); - assertThatThrownBy(() -> distributionSetManagement.deleteMetadata(readOnlyId, mdPresetKey)) - .as("Distribution set not allowed to me modified.") - .isInstanceOf(InsufficientPermissionException.class); - assertThatThrownBy(() -> distributionSetManagement.deleteMetadata(hiddenId, mdPresetKey)) - .as("Distribution set should not be visible.") - .isInstanceOf(EntityNotFoundException.class); - } - - @Test - void verifyTagFilteringAndManagement() { - // permit all operations first to prepare test setup - permitAllOperations(AccessController.Operation.READ); - permitAllOperations(AccessController.Operation.CREATE); - permitAllOperations(AccessController.Operation.UPDATE); - - final DistributionSet permitted = testdataFactory.createDistributionSet(); - final DistributionSet readOnly = testdataFactory.createDistributionSet(); - final DistributionSet hidden = testdataFactory.createDistributionSet(); - final Long dsTagId = distributionSetTagManagement.create(entityFactory.tag().create().name("dsTag")).getId(); - final Long dsTag2Id = distributionSetTagManagement.create(entityFactory.tag().create().name("dsTag2")).getId(); - - // perform tag assignment before setting access rules - distributionSetManagement.assignTag(Arrays.asList(permitted.getId(), readOnly.getId(), hidden.getId()), - dsTagId); - // entities created - reset rules - testAccessControlManger.deleteAllRules(); - - // define access controlling rule - defineAccess(AccessController.Operation.READ, permitted, readOnly); - - // allow updating the permitted distributionSet - defineAccess(AccessController.Operation.UPDATE, permitted); - - assertThat(distributionSetManagement.findByTag(dsTagId, Pageable.unpaged()).get().map(Identifiable::getId) - .toList()).containsOnly(permitted.getId(), readOnly.getId()); - - assertThat(distributionSetManagement.findByRsqlAndTag("name==*", dsTagId, Pageable.unpaged()).get() - .map(Identifiable::getId).toList()).containsOnly(permitted.getId(), readOnly.getId()); - - // verify distributionSetManagement#unassignTag on permitted target - assertThat(distributionSetManagement - .unassignTag(Collections.singletonList(permitted.getId()), dsTagId)) - .size() - .isEqualTo(1); - // verify distributionSetManagement#assignTag on permitted target - assertThat(distributionSetManagement.assignTag(Collections.singletonList(permitted.getId()), dsTagId)) - .hasSize(1); - // verify distributionSetManagement#unAssignTag on permitted target - assertThat(distributionSetManagement.unassignTag(List.of(permitted.getId()), dsTagId) - .get(0).getId()) - .isEqualTo(permitted.getId()); - - // assignment is denied for readOnlyTarget (read, but no update permissions) - final List readOblyList = Collections.singletonList(readOnly.getId()); - assertThatThrownBy(() -> - distributionSetManagement.unassignTag(readOblyList, dsTagId)) - .as("Missing update permissions for target to toggle tag assignment.") - .isInstanceOf(InsufficientPermissionException.class); - - // assignment is denied for readOnlyTarget (read, but no update permissions) - // dsTag2- since - it is tagged with dsTag and won't do anything if assigning dsTag - assertThatThrownBy(() -> { - distributionSetManagement.assignTag(readOblyList, dsTag2Id); - }).as("Missing update permissions for target to toggle tag assignment.") - .isInstanceOf(InsufficientPermissionException.class); - - // assignment is denied for hiddenTarget since it's hidden - final List hiddenList = Collections.singletonList(hidden.getId()); - assertThatThrownBy(() -> distributionSetManagement.unassignTag(hiddenList, dsTagId)) - .as("Missing update permissions for target to toggle tag assignment.") - .isInstanceOf(EntityNotFoundException.class); - - // assignment is denied for hiddenTarget since it's hidden - assertThatThrownBy(() -> { - distributionSetManagement.assignTag(hiddenList, dsTagId); - }).as("Missing update permissions for target to toggle tag assignment.") - .isInstanceOf(EntityNotFoundException.class); - - // assignment is denied for hiddenTarget since it's hidden - final List hiddenIdList = List.of(hidden.getId()); - assertThatThrownBy(() -> distributionSetManagement.unassignTag(hiddenIdList, dsTagId)) - .as("Missing update permissions for target to toggle tag assignment.") - .isInstanceOf(EntityNotFoundException.class); - } - - @Test - void verifyAutoAssignmentUsage() { - // permit all operations first to prepare test setup - permitAllOperations(AccessController.Operation.READ); - permitAllOperations(AccessController.Operation.CREATE); - permitAllOperations(AccessController.Operation.UPDATE); - - final DistributionSet permitted = testdataFactory.createDistributionSet(); - final DistributionSet readOnly = testdataFactory.createDistributionSet(); - final DistributionSet hidden = testdataFactory.createDistributionSet(); - // has to lock them, otherwise implicit lock shall be made which require DistributionSet update permissions - distributionSetManagement.lock(permitted.getId()); - distributionSetManagement.lock(readOnly.getId()); - distributionSetManagement.lock(hidden.getId()); - - // entities created - reset rules - testAccessControlManger.deleteAllRules(); - // define read access - defineAccess(AccessController.Operation.READ, permitted, readOnly); - // permit update operation - defineAccess(AccessController.Operation.UPDATE, permitted); - - final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name("test").query("id==*")); - - assertThat(targetFilterQueryManagement - .updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()).ds(permitted.getId()) - .actionType(Action.ActionType.FORCED).confirmationRequired(false)) - .getAutoAssignDistributionSet().getId()).isEqualTo(permitted.getId()); - targetFilterQueryManagement - .updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()) - .ds(readOnly.getId()).actionType(Action.ActionType.FORCED).confirmationRequired(false)) - .getAutoAssignDistributionSet().getId(); - final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate = new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()) - .ds(hidden.getId()).actionType(Action.ActionType.FORCED).confirmationRequired(false); - assertThatThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(autoAssignDistributionSetUpdate)) - .isInstanceOf(EntityNotFoundException.class); - } - - private void defineAccess(final AccessController.Operation operation, final DistributionSet... distributionSets) { - defineAccess(operation, List.of(distributionSets)); - } - - private void defineAccess(final AccessController.Operation operation, final List targets) { - final List ids = targets.stream().map(DistributionSet::getId).toList(); - testAccessControlManger.defineAccessRule( - JpaDistributionSet.class, operation, - dsByIds(ids), - distributionSet -> ids.contains(distributionSet.getId())); - } - - private static Specification dsByIds(final Collection distids) { - return (dsRoot, query, cb) -> { - final Predicate predicate = dsRoot.get(JpaDistributionSet_.id).in(distids); - query.distinct(true); - return predicate; - }; - } -} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/TargetAccessControllerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/TargetAccessControllerTest.java deleted file mode 100644 index a052e481e..000000000 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/TargetAccessControllerTest.java +++ /dev/null @@ -1,418 +0,0 @@ -/** - * Copyright (c) 2023 Bosch.IO GmbH and others - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.hawkbit.repository.jpa.acm.controller; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import jakarta.persistence.criteria.Predicate; - -import org.eclipse.hawkbit.repository.FilterParams; -import org.eclipse.hawkbit.repository.Identifiable; -import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; -import org.eclipse.hawkbit.repository.jpa.acm.AccessController; -import org.eclipse.hawkbit.repository.jpa.autoassign.AutoAssignChecker; -import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; -import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet_; -import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; -import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications; -import org.eclipse.hawkbit.repository.model.Action; -import org.eclipse.hawkbit.repository.model.DistributionSet; -import org.eclipse.hawkbit.repository.model.Rollout; -import org.eclipse.hawkbit.repository.model.RolloutGroup; -import org.eclipse.hawkbit.repository.model.Target; -import org.eclipse.hawkbit.repository.model.TargetFilterQuery; -import org.eclipse.hawkbit.repository.model.TargetUpdateStatus; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.domain.Specification; - -/** - * Feature: Component Tests - Access Control
- * Story: Test Target Access Controller - */ -class TargetAccessControllerTest extends AbstractAccessControllerTest { - - @Autowired - AutoAssignChecker autoAssignChecker; - - /** - * Verifies read access rules for targets - */ - @Test - void verifyTargetReadOperations() { - permitAllOperations(AccessController.Operation.CREATE); - - final Target permittedTarget = targetManagement - .create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED)); - - final Target hiddenTarget = targetManagement - .create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED)); - - // define access controlling rule - defineAccess(AccessController.Operation.READ, permittedTarget); - - // verify targetManagement#findAll - assertThat(targetManagement.findAll(Pageable.unpaged()).get().map(Identifiable::getId).toList()) - .containsOnly(permittedTarget.getId()); - - // verify targetManagement#findByRsql - assertThat(targetManagement.findByRsql("id==*", Pageable.unpaged()).get().map(Identifiable::getId).toList()) - .containsOnly(permittedTarget.getId()); - - // verify targetManagement#findByUpdateStatus - assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.REGISTERED, Pageable.unpaged()).get() - .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()); - - // verify targetManagement#getByControllerID - assertThat(targetManagement.getByControllerID(permittedTarget.getControllerId())).isPresent(); - final String hiddenTargetControllerId = hiddenTarget.getControllerId(); - assertThatThrownBy(() -> targetManagement.getByControllerID(hiddenTargetControllerId)) - .as("Missing read permissions for hidden target.") - .isInstanceOf(InsufficientPermissionException.class); - - // verify targetManagement#getByControllerID - assertThat(targetManagement - .getByControllerID(Arrays.asList(permittedTarget.getControllerId(), hiddenTargetControllerId)) - .stream().map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()); - - // verify targetManagement#get - assertThat(targetManagement.get(permittedTarget.getId())).isPresent(); - assertThat(targetManagement.get(hiddenTarget.getId())).isEmpty(); - - // verify targetManagement#get - assertThat(targetManagement.get(Arrays.asList(permittedTarget.getId(), hiddenTarget.getId())).stream() - .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()); - - // verify targetManagement#getControllerAttributes - assertThat(targetManagement.getControllerAttributes(permittedTarget.getControllerId())).isEmpty(); - assertThatThrownBy(() -> targetManagement.getControllerAttributes(hiddenTargetControllerId)) - .as("Target should not be found.") - .isInstanceOf(InsufficientPermissionException.class); - - final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name("test").query("id==*")); - - // verify targetManagement#findByTargetFilterQuery - assertThat(targetManagement.findByTargetFilterQuery(targetFilterQuery.getId(), Pageable.unpaged()).get() - .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()); - - // verify targetManagement#findByTargetFilterQuery (used by UI) - assertThat(targetManagement.findByFilters(new FilterParams(null, null, null, null), Pageable.unpaged()).get() - .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()); - } - - @Test - void verifyTagFilteringAndManagement() { - // permit all operations first to prepare test setup - permitAllOperations(AccessController.Operation.READ); - permitAllOperations(AccessController.Operation.CREATE); - permitAllOperations(AccessController.Operation.UPDATE); - - final Target permittedTarget = targetManagement - .create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED)); - - final Target readOnlyTarget = targetManagement - .create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED)); - final String readOnlyTargetControllerId = readOnlyTarget.getControllerId(); - - final Target hiddenTarget = targetManagement - .create(entityFactory.target().create().controllerId("device03").status(TargetUpdateStatus.REGISTERED)); - - final Long myTagId = targetTagManagement.create(entityFactory.tag().create().name("myTag")).getId(); - - // perform tag assignment before setting access rules - targetManagement.assignTag(Arrays.asList(permittedTarget.getControllerId(), readOnlyTargetControllerId, - hiddenTarget.getControllerId()), myTagId); - - // define access controlling rule - testAccessControlManger.deleteAllRules(); - defineAccess(AccessController.Operation.READ, permittedTarget, readOnlyTarget); - // allow update operation - // allow update operation - defineAccess(AccessController.Operation.UPDATE, permittedTarget); - - // verify targetManagement#findByTag - assertThat( - targetManagement.findByTag(myTagId, Pageable.unpaged()).get().map(Identifiable::getId).toList()) - .containsOnly(permittedTarget.getId(), readOnlyTarget.getId()); - - // verify targetManagement#findByRsqlAndTag - assertThat(targetManagement.findByRsqlAndTag("id==*", myTagId, Pageable.unpaged()).get() - .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId(), readOnlyTarget.getId()); - - // verify targetManagement#assignTag on permitted target - assertThat(targetManagement.assignTag(Collections.singletonList(permittedTarget.getControllerId()), myTagId)) - .hasSize(1); - // verify targetManagement#unassignTag on permitted target - assertThat(targetManagement.unassignTag(Collections.singletonList(permittedTarget.getControllerId()), myTagId)) - .hasSize(1); - // verify targetManagement#assignTag on permitted target - assertThat(targetManagement.assignTag(Collections.singletonList(permittedTarget.getControllerId()), myTagId)) - .hasSize(1); - // verify targetManagement#unAssignTag on permitted target - assertThat(targetManagement.unassignTag(List.of(permittedTarget.getControllerId()), myTagId).get(0).getControllerId()) - .isEqualTo(permittedTarget.getControllerId()); - - // assignment is denied for readOnlyTarget (read, but no update permissions) - // No exception has been thrown - because no real change is done -// assertThatThrownBy(() -> { -// targetManagement -// .assignTag(List.of(readOnlyTarget.getControllerId()), myTag.getId()) -// .getUnassigned(); -// }).as("Missing update permissions for target to toggle tag assignment.") -// .isInstanceOf(InsufficientPermissionException.class); - - // assignment is denied for readOnlyTarget (read, but no update permissions) - final List readTargetControllerIdList = Collections.singletonList(readOnlyTargetControllerId); - assertThatThrownBy(() -> targetManagement.assignTag(readTargetControllerIdList, myTagId)) - .as("Missing update permissions for target to toggle tag assignment.") - .isInstanceOfAny(InsufficientPermissionException.class); - - // assignment is denied for readOnlyTarget (read, but no update permissions) - final List readOnlyTargetControllerIdList = List.of(readOnlyTargetControllerId); - assertThatThrownBy(() -> targetManagement.unassignTag(readOnlyTargetControllerIdList, myTagId)) - .as("Missing update permissions for target to toggle tag assignment.") - .isInstanceOf(InsufficientPermissionException.class); - - // assignment is denied for hiddenTarget since it's hidden - final List hiddenTargetControllerIdList = Collections.singletonList(hiddenTarget.getControllerId()); - assertThatThrownBy(() -> targetManagement.assignTag(hiddenTargetControllerIdList, myTagId)) - .as("Missing update permissions for target to toggle tag assignment.") - .isInstanceOf(InsufficientPermissionException.class); - - // assignment is denied for hiddenTarget since it's hidden - assertThatThrownBy(() -> targetManagement.assignTag(hiddenTargetControllerIdList, myTagId)) - .as("Missing update permissions for target to toggle tag assignment.") - .isInstanceOf(InsufficientPermissionException.class); - - // assignment is denied for hiddenTarget since it's hidden - assertThatThrownBy(() -> targetManagement.unassignTag(hiddenTargetControllerIdList, myTagId)) - .as("Missing update permissions for target to toggle tag assignment.") - .isInstanceOf(InsufficientPermissionException.class); - } - - /** - * Verifies rules for target assignment - */ - @Test - void verifyTargetAssignment() { - permitAllOperations(AccessController.Operation.READ); - permitAllOperations(AccessController.Operation.CREATE); - permitAllOperations(AccessController.Operation.UPDATE); - - final Long dsId = testdataFactory.createDistributionSet("myDs").getId(); - distributionSetManagement.lock(dsId); - // entities created - reset rules - testAccessControlManger.deleteAllRules(); - - permitAllOperations(AccessController.Operation.READ); - permitAllOperations(AccessController.Operation.CREATE); - - final Target permittedTarget = targetManagement - .create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED)); - final String hiddenTargetControllerId = targetManagement - .create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED)) - .getControllerId(); - - // define access controlling rule - overwriteAccess(AccessController.Operation.READ, permittedTarget); - - // verify targetManagement#findByUpdateStatus before assignment - assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.REGISTERED, Pageable.unpaged()).get() - .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()); - - testAccessControlManger.defineAccessRule( - JpaTarget.class, AccessController.Operation.UPDATE, - TargetSpecifications.hasId(permittedTarget.getId()), - target -> target.getId().equals(permittedTarget.getId())); - - assertThat(assignDistributionSet(dsId, permittedTarget.getControllerId()).getAssigned()).isEqualTo(1); - // assigning of non allowed target behaves as not found - assertThatThrownBy(() -> assignDistributionSet(dsId, hiddenTargetControllerId)).isInstanceOf(AssertionError.class); - - // verify targetManagement#findByUpdateStatus(REGISTERED) after assignment - assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.REGISTERED, Pageable.unpaged()) - .getTotalElements()).isZero(); - - // verify targetManagement#findByUpdateStatus(PENDING) after assignment - assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.PENDING, Pageable.unpaged()).get() - .map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()); - } - - /** - * Verifies rules for target assignment - */ - @Test - void verifyTargetAssignmentOnNonUpdatableTarget() { - permitAllOperations(AccessController.Operation.READ); - permitAllOperations(AccessController.Operation.CREATE); - permitAllOperations(AccessController.Operation.UPDATE); - - final Long firstDsId = testdataFactory.createDistributionSet("myDs").getId(); - distributionSetManagement.lock(firstDsId); - final DistributionSet secondDs = testdataFactory.createDistributionSet("anotherDs"); - distributionSetManagement.lock(secondDs.getId()); - // entities created - reset rules - testAccessControlManger.deleteAllRules(); - - permitAllOperations(AccessController.Operation.READ); - permitAllOperations(AccessController.Operation.CREATE); - - final Target manageableTarget = targetManagement - .create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED)); - final Target readOnlyTarget = targetManagement - .create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED)); - - // overwriting full access controlling rule - overwriteAccess(AccessController.Operation.READ, manageableTarget, readOnlyTarget); - overwriteAccess(AccessController.Operation.UPDATE, manageableTarget); - - // assignment is permitted for manageableTarget - assertThat(assignDistributionSet(firstDsId, manageableTarget.getControllerId()).getAssigned()).isEqualTo(1); - - // assignment is denied for readOnlyTarget (read, but no update permissions) - final var readOnlyTargetControllerId = readOnlyTarget.getControllerId(); - assertThatThrownBy(() -> assignDistributionSet(firstDsId, readOnlyTargetControllerId)).isInstanceOf(AssertionError.class); - - // bunch assignment skips denied denied since at least one target without update - // permissions is present - assertThat(assignDistributionSet(secondDs.getId(), - Arrays.asList(readOnlyTargetControllerId, manageableTarget.getControllerId()), - Action.ActionType.FORCED).getAssigned()).isEqualTo(1); - } - - /** - * Verifies only manageable targets are part of the rollout - */ - @Test - void verifyRolloutTargetScope() { - permitAllOperations(AccessController.Operation.READ); - permitAllOperations(AccessController.Operation.CREATE); - permitAllOperations(AccessController.Operation.UPDATE); - final DistributionSet ds = testdataFactory.createDistributionSet("myDs"); - distributionSetManagement.lock(ds.getId()); - // entities created - reset rules - testAccessControlManger.deleteAllRules(); - - permitAllOperations(AccessController.Operation.READ); - permitAllOperations(AccessController.Operation.CREATE); - - final List updateTargets = testdataFactory.createTargets("update1", "update2", "update3"); - final List readTargets = testdataFactory.createTargets("read1", "read2", "read3", "read4"); - final List hiddenTargets = testdataFactory.createTargets("hidden1", "hidden2", "hidden3", "hidden4", "hidden5"); - - defineAccess(AccessController.Operation.UPDATE, updateTargets); - overwriteAccess(AccessController.Operation.READ, merge(readTargets, updateTargets)); - - final Rollout rollout = testdataFactory.createRolloutByVariables( - "testRollout", "description", updateTargets.size(), "id==*", ds, "50", "5"); - - assertThat(rollout.getTotalTargets()).isEqualTo(updateTargets.size()); - - final List content = rolloutGroupManagement.findByRollout(rollout.getId(), Pageable.unpaged()).getContent(); - assertThat(content).hasSize(updateTargets.size()); - - final List rolloutTargets = content.stream().flatMap( - group -> rolloutGroupManagement.findTargetsOfRolloutGroup(group.getId(), Pageable.unpaged()).get()) - .toList(); - - assertThat(rolloutTargets).hasSize(updateTargets.size()).allMatch( - target -> updateTargets.stream().anyMatch(readTarget -> readTarget.getId().equals(target.getId()))) - .noneMatch(target -> readTargets.stream() - .anyMatch(readTarget -> readTarget.getId().equals(target.getId()))) - .noneMatch(target -> hiddenTargets.stream() - .anyMatch(readTarget -> readTarget.getId().equals(target.getId()))); - } - - /** - * Verifies only manageable targets are part of an auto assignment. - */ - @Test - void verifyAutoAssignmentTargetScope() { - permitAllOperations(AccessController.Operation.READ); - permitAllOperations(AccessController.Operation.CREATE); - permitAllOperations(AccessController.Operation.UPDATE); - final DistributionSet distributionSet = testdataFactory.createDistributionSet(); - distributionSetManagement.lock(distributionSet.getId()); - // entities created - reset rules - testAccessControlManger.deleteAllRules(); - - permitAllOperations(AccessController.Operation.CREATE); - - final List updateTargets = testdataFactory.createTargets("update1", "update2", "update3"); - final List readTargets = testdataFactory.createTargets("read1", "read2", "read3", "read4"); - final List hiddenTargets = testdataFactory.createTargets("hidden1", "hidden2", "hidden3", "hidden4", - "hidden5"); - - defineAccess(AccessController.Operation.UPDATE, updateTargets); - defineAccess(AccessController.Operation.READ, merge(updateTargets, readTargets)); - - final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement - .create(entityFactory.targetFilterQuery().create().name("testName").query("id==*")); - - testAccessControlManger.defineAccessRule( - JpaDistributionSet.class, AccessController.Operation.READ, - dsById(distributionSet.getId()), - ds -> ds.getId().equals(distributionSet.getId())); - - targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery() - .updateAutoAssign(targetFilterQuery.getId()).ds(distributionSet.getId())); - - autoAssignChecker.checkAllTargets(); - - assertThat(targetManagement.findByAssignedDistributionSet(distributionSet.getId(), Pageable.unpaged()) - .getContent()) - .hasSize(updateTargets.size()) - .allMatch(assignedTarget -> updateTargets.stream() - .anyMatch(updateTarget -> updateTarget.getId().equals(assignedTarget.getId()))) - .noneMatch(assignedTarget -> readTargets.stream() - .anyMatch(updateTarget -> updateTarget.getId().equals(assignedTarget.getId()))) - .noneMatch(assignedTarget -> hiddenTargets.stream() - .anyMatch(updateTarget -> updateTarget.getId().equals(assignedTarget.getId()))); - } - - private void defineAccess(final AccessController.Operation operation, final Target... target) { - defineAccess(operation, List.of(target)); - } - - private void defineAccess(final AccessController.Operation operation, final List targets) { - final List ids = targets.stream().map(Target::getId).toList(); - testAccessControlManger.defineAccessRule( - JpaTarget.class, operation, - TargetSpecifications.hasIdIn(ids), - target -> ids.contains(target.getId())); - } - - private void overwriteAccess(final AccessController.Operation operation, final Target... target) { - overwriteAccess(operation, List.of(target)); - } - - private void overwriteAccess(final AccessController.Operation operation, final List targets) { - final List ids = targets.stream().map(Target::getId).toList(); - testAccessControlManger.overwriteAccessRule( - JpaTarget.class, operation, - TargetSpecifications.hasIdIn(ids), - target -> ids.contains(target.getId())); - } - - private static Specification dsById(final Long distid) { - return (dsRoot, query, cb) -> { - final Predicate predicate = cb.equal(dsRoot.get(JpaDistributionSet_.id), distid); - query.distinct(true); - return predicate; - }; - } -} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/TargetTypeAccessControllerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/TargetTypeAccessControllerTest.java deleted file mode 100644 index 7519d0fb2..000000000 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/TargetTypeAccessControllerTest.java +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Copyright (c) 2023 Bosch.IO GmbH and others - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.hawkbit.repository.jpa.acm.controller; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Stream; - -import org.eclipse.hawkbit.repository.Identifiable; -import org.eclipse.hawkbit.repository.builder.TargetTypeCreate; -import org.eclipse.hawkbit.repository.builder.TargetTypeUpdate; -import org.eclipse.hawkbit.repository.exception.EntityNotFoundException; -import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; -import org.eclipse.hawkbit.repository.jpa.acm.AccessController; -import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType; -import org.eclipse.hawkbit.repository.jpa.specifications.TargetTypeSpecification; -import org.eclipse.hawkbit.repository.model.TargetType; -import org.junit.jupiter.api.Test; -import org.springframework.data.domain.Pageable; - -/** - * Feature: Component Tests - Access Control
- * Story: Test Target Type Access Controller - */ -class TargetTypeAccessControllerTest extends AbstractAccessControllerTest { - - /** - * Verifies read access rules for target types - */ - @Test - void verifyTargetTypeReadOperations() { - permitAllOperations(AccessController.Operation.CREATE); - final TargetType permittedTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type1")); - final TargetType hiddenTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type2")); - - // define access controlling rule - defineAccess(AccessController.Operation.READ, permittedTargetType); - - // verify targetTypeManagement#findAll - assertThat(targetTypeManagement.findAll(Pageable.unpaged()).get().map(Identifiable::getId).toList()) - .containsOnly(permittedTargetType.getId()); - - // verify targetTypeManagement#findByRsql - assertThat(targetTypeManagement.findByRsql("name==*", Pageable.unpaged()).get().map(Identifiable::getId).toList()) - .containsOnly(permittedTargetType.getId()); - - // verify targetTypeManagement#findByName - assertThat(targetTypeManagement.findByName(permittedTargetType.getName(), Pageable.unpaged()).getContent()) - .hasSize(1).satisfies(results -> - assertThat(results.get(0).getId()).isEqualTo(permittedTargetType.getId())); - assertThat(targetTypeManagement.findByName(hiddenTargetType.getName(), Pageable.unpaged())).isEmpty(); - - // verify targetTypeManagement#count - assertThat(targetTypeManagement.count()).isEqualTo(1); - - // verify targetTypeManagement#countByName - assertThat(targetTypeManagement.countByName(permittedTargetType.getName())).isEqualTo(1); - assertThat(targetTypeManagement.countByName(hiddenTargetType.getName())).isZero(); - - // verify targetTypeManagement#countByName - assertThat(targetTypeManagement.countByName(permittedTargetType.getName())).isEqualTo(1); - assertThat(targetTypeManagement.countByName(hiddenTargetType.getName())).isZero(); - - // verify targetTypeManagement#get by id - assertThat(targetTypeManagement.get(permittedTargetType.getId())).isPresent(); - final Long hiddenTargetTypeId = hiddenTargetType.getId(); - assertThat(targetTypeManagement.get(hiddenTargetTypeId)).isEmpty(); - - // verify targetTypeManagement#getByName - assertThat(targetTypeManagement.getByName(permittedTargetType.getName())).isPresent(); - assertThat(targetTypeManagement.getByName(hiddenTargetType.getName())).isEmpty(); - - // verify targetTypeManagement#get by ids - assertThat(targetTypeManagement.get(Arrays.asList(permittedTargetType.getId(), hiddenTargetTypeId)) - .stream().map(Identifiable::getId).toList()).containsOnly(permittedTargetType.getId()); - - // verify targetTypeManagement#update is not possible. Assert exception thrown. - final TargetTypeUpdate targetTypeUpdate = entityFactory.targetType().update(hiddenTargetTypeId) - .name(hiddenTargetType.getName() + "/new").description("newDesc"); - assertThatThrownBy(() -> targetTypeManagement.update(targetTypeUpdate)) - .as("Target type update shouldn't be allowed since the target type is not visible.") - .isInstanceOf(EntityNotFoundException.class); - - // verify targetTypeManagement#delete is not possible. Assert exception thrown. - assertThatThrownBy(() -> targetTypeManagement.delete(hiddenTargetTypeId)) - .as("Target type delete shouldn't be allowed since the target type is not visible.") - .isInstanceOf(EntityNotFoundException.class); - } - - /** - * Verifies delete access rules for target types - */ - @Test - void verifyTargetTypeDeleteOperations() { - permitAllOperations(AccessController.Operation.CREATE); - final TargetType manageableTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type1")); - - final TargetType readOnlyTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type2")); - - // define access controlling rule to allow reading both types - defineAccess(AccessController.Operation.READ, manageableTargetType, readOnlyTargetType); - - // permit operation to delete permittedTargetType - defineAccess(AccessController.Operation.DELETE, manageableTargetType); - - // delete the manageableTargetType - targetTypeManagement.delete(manageableTargetType.getId()); - - // verify targetTypeManagement#delete for readOnlyTargetType is not possible - final Long readOnlyTargetTypeId = readOnlyTargetType.getId(); - assertThatThrownBy(() -> targetTypeManagement.delete(readOnlyTargetTypeId)) - .isInstanceOfAny(InsufficientPermissionException.class, EntityNotFoundException.class); - } - - /** - * Verifies update operation for target types - */ - @Test - void verifyTargetTypeUpdateOperations() { - permitAllOperations(AccessController.Operation.CREATE); - final TargetType manageableTargetType = targetTypeManagement - .create(entityFactory.targetType().create().name("type1")); - - final TargetType readOnlyTargetType = targetTypeManagement - .create(entityFactory.targetType().create().name("type2")); - - // define access controlling rule to allow reading both types - defineAccess(AccessController.Operation.READ, manageableTargetType, readOnlyTargetType); - - // permit updating the manageableTargetType - defineAccess(AccessController.Operation.UPDATE, manageableTargetType); - - // update the manageableTargetType - targetTypeManagement.update(entityFactory.targetType().update(manageableTargetType.getId()) - .name(manageableTargetType.getName() + "/new").description("newDesc")); - - // verify targetTypeManagement#update for readOnlyTargetType is not possible - final TargetTypeUpdate targetTypeUpdate = entityFactory.targetType().update(readOnlyTargetType.getId()) - .name(readOnlyTargetType.getName() + "/new").description("newDesc"); - assertThatThrownBy(() -> targetTypeManagement.update(targetTypeUpdate)) - .isInstanceOf(InsufficientPermissionException.class); - } - - /** - * Verifies create operation blocked by controller - */ - @Test - void verifyTargetTypeCreationBlockedByAccessController() { - defineAccess(AccessController.Operation.CREATE); // allows for none - // verify targetTypeManagement#create for any type - final TargetTypeCreate targetTypeCreate = entityFactory.targetType().create().name("type1"); - assertThatThrownBy(() -> targetTypeManagement.create(targetTypeCreate)) - .as("Target type create shouldn't be allowed since the target type is not visible.") - .isInstanceOf(InsufficientPermissionException.class); - } - - private void defineAccess(final AccessController.Operation operation, final TargetType... targetTypes) { - final List ids = Stream.of(targetTypes).map(TargetType::getId).toList(); - testAccessControlManger.defineAccessRule( - JpaTargetType.class, operation, - TargetTypeSpecification.hasIdIn(ids), - targetType -> ids.contains(targetType.getId())); - } -} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/TestAccessControlManger.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/TestAccessControlManger.java deleted file mode 100644 index d6d3072ae..000000000 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/controller/TestAccessControlManger.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) 2023 Bosch.IO GmbH and others - * - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.eclipse.hawkbit.repository.jpa.acm.controller; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Predicate; - -import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException; -import org.eclipse.hawkbit.repository.jpa.acm.AccessController; -import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaBaseEntity; -import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaBaseEntity_; -import org.springframework.data.jpa.domain.Specification; - -public class TestAccessControlManger { - - private final Map, AccessRule> accessRules = new HashMap<>(); - - public void deleteAllRules() { - accessRules.clear(); - } - - public void defineAccessRule( - final Class ruleClass, final AccessController.Operation operation, - final Specification specification, final Predicate check) { - defineAccessRule(ruleClass, operation, specification, check, false); - } - - public void overwriteAccessRule( - final Class ruleClass, final AccessController.Operation operation, - final Specification specification, final Predicate check) { - defineAccessRule(ruleClass, operation, specification, check, true); - } - - private void defineAccessRule( - final Class ruleClass, final AccessController.Operation operation, - final Specification specification, final Predicate check, final boolean overwrite) { - final AccessRuleId ruleId = new AccessRuleId<>(ruleClass, operation); - if (!overwrite && accessRules.containsKey(ruleId)) { - throw new IllegalStateException("Access rule already defined for " + ruleId + "! You should explicitly set overwrite to true."); - } - accessRules.put(ruleId, new AccessRule<>(specification, check)); - } - - public Specification getAccessRule(final Class ruleClass, - final AccessController.Operation operation) { - @SuppressWarnings("unchecked") - final AccessRule accessRule = (AccessRule) accessRules.getOrDefault(new AccessRuleId<>(ruleClass, operation), null); - if (accessRule == null) { - return nop(); - } else { - return accessRule.specification(); - } - } - - public void assertOperation(final Class ruleClass, final AccessController.Operation operation, final List entities) { - @SuppressWarnings("unchecked") - final AccessRule accessRule = (AccessRule) accessRules.getOrDefault(new AccessRuleId<>(ruleClass, operation), null); - if (accessRule == null) { - throw new InsufficientPermissionException("No access define - reject all"); - } else { - for (final T entity : entities) { - if (!accessRule.checker.test(entity)) { - throw new InsufficientPermissionException("Access to " + ruleClass.getName() + "/" + entity + " not allowed by checker!"); - } - } - } - } - - private static Specification nop() { - return (targetRoot, query, cb) -> cb.equal(targetRoot.get(AbstractJpaBaseEntity_.id), -1); - } - - private record AccessRuleId(Class ruleClass, AccessController.Operation operation) {} - - private record AccessRule(Specification specification, Predicate checker) {} -} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ArtifactManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ArtifactManagementTest.java index bfb29e4f6..bd798a783 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ArtifactManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ArtifactManagementTest.java @@ -253,8 +253,6 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest { /** * Test method for {@link org.eclipse.hawkbit.repository.ArtifactManagement#delete(long)}. - */ - /** * Tests the deletion of a local artifact including metadata. */ @Test @@ -585,7 +583,7 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest { } private T runAsTenant(final String tenant, final Callable callable) throws Exception { - return SecurityContextSwitch.runAs(SecurityContextSwitch.withUserAndTenantAllSpPermissions("user", tenant), callable); + return SecurityContextSwitch.callAs(SecurityContextSwitch.withUserAndTenantAllSpPermissions("user", tenant), callable); } private SoftwareModule createSoftwareModuleForTenant(final String tenant) throws Exception { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ControllerManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ControllerManagementTest.java index 2de884c28..0388e6b86 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ControllerManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/ControllerManagementTest.java @@ -16,6 +16,7 @@ import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpre import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS; import static org.eclipse.hawkbit.repository.jpa.configuration.Constants.TX_RT_MAX; import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY; +import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs; import static org.eclipse.hawkbit.repository.test.util.TestdataFactory.DEFAULT_CONTROLLER_ID; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; @@ -107,11 +108,9 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { testdataFactory.createTarget(controllerId); final WithUser withController = SecurityContextSwitch.withController("controller", CONTROLLER_ROLE_ANONYMOUS); - assertThatExceptionOfType(AssignmentQuotaExceededException.class).isThrownBy(() -> SecurityContextSwitch - .runAs(withController, () -> { - writeAttributes(controllerId, allowedAttributes + 1, "key", "value"); - return null; - })).withMessageContaining("" + allowedAttributes); + assertThatExceptionOfType(AssignmentQuotaExceededException.class) + .isThrownBy(() -> runAs(withController, () -> writeAttributes(controllerId, allowedAttributes + 1, "key", "value"))) + .withMessageContaining("" + allowedAttributes); // verify that no attributes have been written assertThat(targetManagement.getControllerAttributes(controllerId)).isEmpty(); @@ -121,13 +120,12 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { SecurityContextSwitch.runAs(withController, () -> { writeAttributes(controllerId, allowedAttributes, "key", "value1"); writeAttributes(controllerId, allowedAttributes, "key", "value2"); - return null; }); assertThat(targetManagement.getControllerAttributes(controllerId)).hasSize(10); // Now rite one more assertThatExceptionOfType(AssignmentQuotaExceededException.class).isThrownBy(() -> SecurityContextSwitch - .runAs(withController, () -> { + .getAs(withController, () -> { writeAttributes(controllerId, 1, "additional", "value1"); return null; })).withMessageContaining("" + allowedAttributes); @@ -185,7 +183,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { final Long actionId = createTargetAndAssignDs(); SecurityContextSwitch - .runAs(SecurityContextSwitch.withController("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { + .getAs(SecurityContextSwitch.withController("controller", CONTROLLER_ROLE_ANONYMOUS), () -> { // Fails as one entry is already in there from the assignment assertThatExceptionOfType(AssignmentQuotaExceededException.class) .isThrownBy(() -> writeStatus(actionId, allowStatusEntries)) @@ -345,8 +343,8 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { } /** - * Verifies that management queries react as specified on calls for non existing entities - * by means of throwing EntityNotFoundException. + * Verifies that management queries react as specified on calls for non existing entities + * by means of throwing EntityNotFoundException. */ @Test @ExpectEvents({ @@ -571,7 +569,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { } /** - * Controller rejects action cancellation with CANCEL_REJECTED status. Action goes back to RUNNING status as it expects + * Controller rejects action cancellation with CANCEL_REJECTED status. Action goes back to RUNNING status as it expects * that the controller will continue the original update. */ @Test @@ -639,8 +637,8 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { } /** - * Verifies that assignment verification works based on SHA1 hash. By design it is not important which artifact - * is actually used for the check as long as they have an identical binary, i.e. same SHA1 hash. + * Verifies that assignment verification works based on SHA1 hash. By design it is not important which artifact + * is actually used for the check as long as they have an identical binary, i.e. same SHA1 hash. */ @Test @ExpectEvents({ @@ -878,7 +876,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { } /** - * Register a controller which does not exist, when a ConcurrencyFailureException is raised, the + * Register a controller which does not exist, when a ConcurrencyFailureException is raised, the * exception is not rethrown when the max retries are not yet reached */ @Test @@ -957,7 +955,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { } /** - * Retry is aborted when an unchecked exception is thrown and the exception should also be + * Retry is aborted when an unchecked exception is thrown and the exception should also be * rethrown */ @Test @@ -1004,7 +1002,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { @Test @ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1) }) @SuppressWarnings("java:S2699") - // java:S2699 - test tests the fired events, no need for assert + // java:S2699 - test tests the fired events, no need for assert void targetPollEventNotSendIfDisabled() { repositoryProperties.setPublishTargetPollEvent(false); controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST); @@ -1121,7 +1119,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { } /** - * Controller tries to send an update feedback after it has been finished which is accepted as the repository is + * Controller tries to send an update feedback after it has been finished which is accepted as the repository is * configured to accept them. */ @Test @@ -1160,7 +1158,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { final String controllerId = "test123"; final Target target = testdataFactory.createTarget(controllerId); - SecurityContextSwitch.runAs(SecurityContextSwitch.withController( + SecurityContextSwitch.getAs(SecurityContextSwitch.withController( "controller", CONTROLLER_ROLE_ANONYMOUS, SpPermission.READ_TARGET), () -> { addAttributeAndVerify(controllerId); @@ -1289,7 +1287,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { } /** - * Verifies that quota is asserted when a controller reports too many DOWNLOADED events for a + * Verifies that quota is asserted when a controller reports too many DOWNLOADED events for a * DOWNLOAD_ONLY action. */ @Test @@ -1516,7 +1514,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { } /** - * Verifies that a target can report FINISHED/ERROR updates for DOWNLOAD_ONLY assignments regardless of + * Verifies that a target can report FINISHED/ERROR updates for DOWNLOAD_ONLY assignments regardless of * repositoryProperties.rejectActionStatusForClosedAction value. */ @Test @@ -1565,7 +1563,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest { /** * Verifies that a controller can report a FINISHED event for a DOWNLOAD_ONLY action after having - * installed an intermediate update. + * installed an intermediate update. */ @Test @ExpectEvents({ diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java index 4ef75a779..92c9755e8 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/RolloutManagementTest.java @@ -1442,22 +1442,22 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { // create scheduled rollout fails without handle rollout permission assertThatExceptionOfType(InsufficientPermissionException.class) .as("Insufficient permission exception when startAt and no handle rollout permission") - .isThrownBy(() -> SecurityContextSwitch.runAs( + .isThrownBy(() -> SecurityContextSwitch.getAs( userWithoutHandleRollout, () -> createRolloutWithStartAt(rolloutName, filter, distributionSet, 1L))); // same action succeeds with handle rollout permission - SecurityContextSwitch.runAs( + SecurityContextSwitch.getAs( userWithHandleRollout, () -> createRolloutWithStartAt(rolloutName + "_withStartTime", filter, distributionSet, 1L)); // same action succeeds with system role permission - SecurityContextSwitch.runAs( + SecurityContextSwitch.getAs( userWithSystemRole, () -> createRolloutWithStartAt(rolloutName + "_withStartTimeSystemRole", filter, distributionSet, 1L)); // same action succeeds without handle rollout permission but with null start at - SecurityContextSwitch.runAs( + SecurityContextSwitch.getAs( userWithoutHandleRollout, () -> createRolloutWithStartAt(rolloutName + "_withoutStartTime", filter, distributionSet, null)); // same action succeeds without handle rollout permission but with Long.MAX_VALUE start at - SecurityContextSwitch.runAs( + SecurityContextSwitch.getAs( userWithoutHandleRollout, () -> createRolloutWithStartAt(rolloutName + "_withLongMax", filter, distributionSet, Long.MAX_VALUE)); } @@ -2501,7 +2501,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest { .pollInterval(Duration.ofMillis(500)) .atMost(Duration.ofSeconds(10)) .until(() -> SecurityContextSwitch - .runAsPrivileged( + .callAsPrivileged( () -> rolloutManagement.get(myRolloutId).orElseThrow(NoSuchElementException::new)) .getStatus().equals(RolloutStatus.RUNNING)); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SystemManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SystemManagementTest.java index 760b982fd..1aa17fbf2 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SystemManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/SystemManagementTest.java @@ -137,7 +137,7 @@ class SystemManagementTest extends AbstractJpaIntegrationTest { for (int i = 0; i < tenants; i++) { final String tenantname = "TENANT" + i; - SecurityContextSwitch.runAs(SecurityContextSwitch.withUserAndTenant("bumlux", tenantname, true, true, false, + SecurityContextSwitch.getAs(SecurityContextSwitch.withUserAndTenant("bumlux", tenantname, true, true, false, SpringEvalExpressions.SYSTEM_ROLE), () -> { systemManagement.getTenantMetadataWithoutDetails(); if (artifactSize > 0) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetManagementTest.java index a2106d704..754312b5b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetManagementTest.java @@ -167,15 +167,15 @@ class TargetManagementTest extends AbstractJpaIntegrationTest { .create(entityFactory.target().create().controllerId("targetWithSecurityToken").securityToken("token")); // retrieve security token only with READ_TARGET_SEC_TOKEN permission - final String securityTokenWithReadPermission = SecurityContextSwitch.runAs( + final String securityTokenWithReadPermission = SecurityContextSwitch.getAs( SecurityContextSwitch.withUser("OnlyTargetReadPermission", SpPermission.READ_TARGET_SEC_TOKEN), createdTarget::getSecurityToken); // retrieve security token only with ROLE_TARGET_ADMIN permission - final String securityTokenWithTargetAdminPermission = SecurityContextSwitch.runAs( + final String securityTokenWithTargetAdminPermission = SecurityContextSwitch.getAs( SecurityContextSwitch.withUser("OnlyTargetAdminPermission", SpRole.TARGET_ADMIN), createdTarget::getSecurityToken); // retrieve security token only with ROLE_TENANT_ADMIN permission - final String securityTokenWithTenantAdminPermission = SecurityContextSwitch.runAs( + final String securityTokenWithTenantAdminPermission = SecurityContextSwitch.getAs( SecurityContextSwitch.withUser("OnlyTenantAdminPermission", SpRole.TENANT_ADMIN), createdTarget::getSecurityToken); @@ -184,7 +184,7 @@ class TargetManagementTest extends AbstractJpaIntegrationTest { // retrieve security token without any permissions final String securityTokenWithoutPermission = SecurityContextSwitch - .runAs(SecurityContextSwitch.withUser("NoPermission"), createdTarget::getSecurityToken); + .getAs(SecurityContextSwitch.withUser("NoPermission"), createdTarget::getSecurityToken); assertThat(createdTarget.getSecurityToken()).isEqualTo("token"); assertThat(securityTokenWithReadPermission).isNotNull(); @@ -705,7 +705,7 @@ class TargetManagementTest extends AbstractJpaIntegrationTest { final String knownTargetControllerId = "readTarget"; controllerManagement.findOrRegisterTargetIfItDoesNotExist(knownTargetControllerId, new URI("http://127.0.0.1")); - SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("bumlux", "READ_TARGET"), () -> { + SecurityContextSwitch.getAs(SecurityContextSwitch.withUser("bumlux", "READ_TARGET"), () -> { final Target findTargetByControllerID = targetManagement.getByControllerID(knownTargetControllerId) .orElseThrow(IllegalStateException::new); assertThat(findTargetByControllerID).isNotNull(); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlToSqlTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlToSqlTest.java index 55a7e5eca..e8c0c81eb 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlToSqlTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlToSqlTest.java @@ -19,7 +19,7 @@ import jakarta.persistence.PersistenceContext; import org.eclipse.hawkbit.repository.RsqlQueryField; import org.eclipse.hawkbit.repository.TargetFields; -import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.repository.jpa.JpaRepositoryConfiguration; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.ql.utils.HawkbitQlToSql; import org.eclipse.hawkbit.repository.test.TestConfiguration; @@ -35,7 +35,7 @@ import org.springframework.test.context.ContextConfiguration; "spring.main.allow-bean-definition-overriding=true", "spring.main.banner-mode=off", "logging.level.root=ERROR" }) -@ContextConfiguration(classes = { RepositoryApplicationConfiguration.class, TestConfiguration.class }) +@ContextConfiguration(classes = { JpaRepositoryConfiguration.class, TestConfiguration.class }) @Disabled("For manual run only, while playing around with RSQL to SQL") @SuppressWarnings("java:S2699") // java:S2699 - manual test, don't actually does assertions class RsqlToSqlTest { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/tenancy/MultiTenancyEntityTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/tenancy/MultiTenancyEntityTest.java index 3a1874355..6f42fa09d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/tenancy/MultiTenancyEntityTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/tenancy/MultiTenancyEntityTest.java @@ -113,13 +113,13 @@ class MultiTenancyEntityTest extends AbstractJpaIntegrationTest { // logged in tenant mytenant - check if tenant default data is // autogenerated assertThat(distributionSetTypeManagement.findAll(PAGE)).isEmpty(); - SecurityContextSwitch.runAsPrivileged(() -> + SecurityContextSwitch.callAsPrivileged(() -> assertThat(systemManagement.createTenantMetadata("mytenant").getTenant().toUpperCase()).isEqualTo("mytenant".toUpperCase())); assertThat(distributionSetTypeManagement.findAll(PAGE)).isNotEmpty(); // check that the cache is not getting in the way, i.e. "bumlux" results in bumlux and not mytenant - assertThat(SecurityContextSwitch.runAs( + assertThat(SecurityContextSwitch.getAs( SecurityContextSwitch.withUserAndTenantAllSpPermissions("user", "bumlux"), () -> systemManagement.getTenantMetadataWithoutDetails().getTenant().toUpperCase())) .isEqualTo("bumlux".toUpperCase()); @@ -178,7 +178,7 @@ class MultiTenancyEntityTest extends AbstractJpaIntegrationTest { } private T runAsTenant(final String tenant, final Callable callable) throws Exception { - return SecurityContextSwitch.runAs(SecurityContextSwitch.withUserAndTenantAllSpPermissions("user", tenant), callable); + return SecurityContextSwitch.callAs(SecurityContextSwitch.withUserAndTenantAllSpPermissions("user", tenant), callable); } private Target createTargetForTenant(final String controllerId, final String tenant) throws Exception { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties b/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties index 176de3681..26a433cb5 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties @@ -54,4 +54,7 @@ logging.level.org.eclipse.persistence=ERROR hawkbit.repository.cluster.lock.ttl=1000 hawkbit.repository.cluster.lock.refreshOnRemainMS=200 hawkbit.repository.cluster.lock.refreshOnRemainPercent=10 -hawkbit.repository.cluster.lock.ticPeriodMS=10 \ No newline at end of file +# reduce scheduler tic period to speed up tests +hawkbit.repository.cluster.lock.ticPeriodMS=10 +# disable spring cloud bus for tests +spring.cloud.bus.enabled=false \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java index 0ea13da37..0848b7212 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/AbstractIntegrationTest.java @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.repository.test.util; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_TENANT_CONFIGURATION; import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE; import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.SYSTEM_ROLE; @@ -94,7 +95,6 @@ import org.springframework.test.context.TestPropertySource; @WithUser(principal = "bumlux", allSpPermissions = true, authorities = { CONTROLLER_ROLE, SYSTEM_ROLE }) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @ContextConfiguration(classes = { TestConfiguration.class }) -//@Import(TestChannelBinderConfiguration.class) // destroy the context after each test class because otherwise we get problem when context is // refreshed we e.g. get two instances of CacheManager which leads to very strange test failures. @DirtiesContext(classMode = ClassMode.AFTER_CLASS) @@ -176,7 +176,7 @@ public abstract class AbstractIntegrationTest { @Autowired protected TestdataFactory testdataFactory; - @Autowired + @Autowired(required = false) protected ServiceMatcher serviceMatcher; @Autowired protected ApplicationEventPublisher eventPublisher; @@ -203,21 +203,21 @@ public abstract class AbstractIntegrationTest { final String description = "Updated description."; osType = SecurityContextSwitch - .runAsPrivileged(() -> testdataFactory.findOrCreateSoftwareModuleType(TestdataFactory.SM_TYPE_OS)); - osType = SecurityContextSwitch.runAsPrivileged(() -> softwareModuleTypeManagement + .callAsPrivileged(() -> testdataFactory.findOrCreateSoftwareModuleType(TestdataFactory.SM_TYPE_OS)); + osType = SecurityContextSwitch.callAsPrivileged(() -> softwareModuleTypeManagement .update(entityFactory.softwareModuleType().update(osType.getId()).description(description))); - appType = SecurityContextSwitch.runAsPrivileged( + appType = SecurityContextSwitch.callAsPrivileged( () -> testdataFactory.findOrCreateSoftwareModuleType(TestdataFactory.SM_TYPE_APP, Integer.MAX_VALUE)); - appType = SecurityContextSwitch.runAsPrivileged(() -> softwareModuleTypeManagement + appType = SecurityContextSwitch.callAsPrivileged(() -> softwareModuleTypeManagement .update(entityFactory.softwareModuleType().update(appType.getId()).description(description))); runtimeType = SecurityContextSwitch - .runAsPrivileged(() -> testdataFactory.findOrCreateSoftwareModuleType(TestdataFactory.SM_TYPE_RT)); - runtimeType = SecurityContextSwitch.runAsPrivileged(() -> softwareModuleTypeManagement + .callAsPrivileged(() -> testdataFactory.findOrCreateSoftwareModuleType(TestdataFactory.SM_TYPE_RT)); + runtimeType = SecurityContextSwitch.callAsPrivileged(() -> softwareModuleTypeManagement .update(entityFactory.softwareModuleType().update(runtimeType.getId()).description(description))); - standardDsType = SecurityContextSwitch.runAsPrivileged(() -> testdataFactory.findOrCreateDefaultTestDsType()); + standardDsType = SecurityContextSwitch.callAsPrivileged(() -> testdataFactory.findOrCreateDefaultTestDsType()); // publish the reset counter market event to reset the counters after // setup. The setup is transparent by the test and its @ExpectedEvent @@ -267,6 +267,7 @@ public abstract class AbstractIntegrationTest { private static final Duration AT_LEAST = Duration.ofMillis(Integer.getInteger("hawkbit.it.rest.await.atLeastMs", 5)); private static final Duration POLL_INTERVAL = Duration.ofMillis(Integer.getInteger("hawkbit.it.rest.await.pollIntervalMs", 10)); private static final Duration TIMEOUT = Duration.ofMillis(Integer.getInteger("hawkbit.it.rest.await.timeoutMs", 200)); + // default wait condition factory protected ConditionFactory await() { return Awaitility.await().atLeast(AT_LEAST).pollInterval(POLL_INTERVAL).atMost(TIMEOUT); @@ -413,7 +414,11 @@ public abstract class AbstractIntegrationTest { } protected boolean isConfirmationFlowActive() { - return tenantConfigurationManagement.getConfigurationValue(TenantConfigurationKey.USER_CONFIRMATION_ENABLED, Boolean.class).getValue(); + return SecurityContextSwitch.getAs( + SecurityContextSwitch.withUser("as_system", READ_TENANT_CONFIGURATION), + () -> tenantConfigurationManagement + .getConfigurationValue(TenantConfigurationKey.USER_CONFIRMATION_ENABLED, Boolean.class) + .getValue()); } protected Long getOsModule(final DistributionSet ds) { diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/CleanupTestExecutionListener.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/CleanupTestExecutionListener.java index 87909f2dc..7abe7fc3a 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/CleanupTestExecutionListener.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/CleanupTestExecutionListener.java @@ -25,7 +25,7 @@ public class CleanupTestExecutionListener extends AbstractTestExecutionListener @Override public void afterTestMethod(final TestContext testContext) throws Exception { - SecurityContextSwitch.runAsPrivileged(() -> { + SecurityContextSwitch.callAsPrivileged(() -> { final ApplicationContext applicationContext = testContext.getApplicationContext(); new JpaTestRepositoryManagement(applicationContext.getBean(TenantAwareCacheManager.class), applicationContext.getBean(SystemSecurityContext.class), diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/SecurityContextSwitch.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/SecurityContextSwitch.java index 7c87ce62f..9173c021a 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/SecurityContextSwitch.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/SecurityContextSwitch.java @@ -16,6 +16,7 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.Callable; +import java.util.function.Supplier; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -35,12 +36,12 @@ public class SecurityContextSwitch { private static final WithUser PRIVILEDGED_USER = createWithUser("bumlux", DEFAULT_TENANT, false, true, false, "ROLE_CONTROLLER", "ROLE_SYSTEM_CODE"); - public static T runAsPrivileged(final Callable callable) throws Exception { + public static T callAsPrivileged(final Callable callable) throws Exception { createTenant(DEFAULT_TENANT); - return runAs(PRIVILEDGED_USER, callable); + return callAs(PRIVILEDGED_USER, callable); } - public static T runAs(final WithUser withUser, final Callable callable) throws Exception { + public static T callAs(final WithUser withUser, final Callable callable) throws Exception { final SecurityContext oldContext = SecurityContextHolder.getContext(); setSecurityContext(withUser); if (withUser.autoCreateTenant()) { @@ -53,6 +54,23 @@ public class SecurityContextSwitch { } } + public static T getAs(final WithUser withUser, final Supplier supplier) { + try { + return callAs(withUser, supplier::get); + } catch (final RuntimeException e) { + throw e; + } catch (final Exception e) { + throw new IllegalStateException("Failed to handle all rollouts", e); + } + } + + public static void runAs(final WithUser withUser, final Runnable runnable) { + getAs(withUser, (Supplier) () -> { + runnable.run(); + return null; + }); + } + public static WithUser withController(final String principal, final String... authorities) { return withUserAndTenant(principal, DEFAULT_TENANT, true, false, true, authorities); } diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java index 3d7f39c03..00c398643 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java @@ -10,6 +10,7 @@ package org.eclipse.hawkbit.repository.test.util; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.SYSTEM_ROLE; import java.io.ByteArrayInputStream; import java.io.InputStream; @@ -1018,7 +1019,7 @@ public class TestdataFactory { groupSize, confirmationRequired, conditions, dynamicRolloutGroupTemplate); // Run here, because Scheduler is disabled during tests - rolloutHandler.handleAll(); + rolloutHandleAll(); return rolloutManagement.get(rollout.getId()).get(); } @@ -1127,9 +1128,9 @@ public class TestdataFactory { public Rollout createSoftDeletedRollout(final String prefix) { final Rollout newRollout = createRollout(prefix); rolloutManagement.start(newRollout.getId()); - rolloutHandler.handleAll(); + rolloutHandleAll(); rolloutManagement.delete(newRollout.getId()); - rolloutHandler.handleAll(); + rolloutHandleAll(); return newRollout; } @@ -1247,17 +1248,20 @@ public class TestdataFactory { } private Action sendUpdateActionStatusToTarget(final Status status, final Action updActA, final Collection msgs) { - return controllerManagement.addUpdateActionStatus( - entityFactory.actionStatus().create(updActA.getId()).status(status).messages(msgs)); + return controllerManagement.addUpdateActionStatus(entityFactory.actionStatus().create(updActA.getId()).status(status).messages(msgs)); } private Rollout startAndReloadRollout(final Rollout rollout) { rolloutManagement.start(rollout.getId()); // Run here, because scheduler is disabled during tests - rolloutHandler.handleAll(); + rolloutHandleAll(); return reloadRollout(rollout); } + private void rolloutHandleAll() { + SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("system", SYSTEM_ROLE), rolloutHandler::handleAll); + } + private Rollout reloadRollout(final Rollout rollout) { return rolloutManagement.get(rollout.getId()).orElseThrow(NoSuchElementException::new); } diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/resources/hawkbit-test-defaults.properties b/hawkbit-repository/hawkbit-repository-test/src/main/resources/hawkbit-test-defaults.properties index afe55b269..de2d280b2 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/resources/hawkbit-test-defaults.properties +++ b/hawkbit-repository/hawkbit-repository-test/src/main/resources/hawkbit-test-defaults.properties @@ -79,3 +79,6 @@ hawkbit.server.security.dos.maxActionsPerTarget=20 # Quota - END # Properties that are managed by autoconfigure module at runtime and not available during test - END + +# disable spring cloud bus for tests +spring.cloud.bus.enabled=false \ No newline at end of file diff --git a/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/AbstractRestIntegrationTest.java b/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/AbstractRestIntegrationTest.java index 29d0da3fe..cbe29dac1 100644 --- a/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/AbstractRestIntegrationTest.java +++ b/hawkbit-rest-core/src/test/java/org/eclipse/hawkbit/rest/AbstractRestIntegrationTest.java @@ -9,9 +9,7 @@ */ package org.eclipse.hawkbit.rest; -import java.time.Duration; - -import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.repository.jpa.JpaRepositoryConfiguration; import org.eclipse.hawkbit.repository.test.TestConfiguration; import org.eclipse.hawkbit.repository.test.util.AbstractIntegrationTest; import org.junit.jupiter.api.BeforeEach; @@ -31,7 +29,7 @@ import org.springframework.web.filter.CharacterEncodingFilter; */ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK) @ContextConfiguration(classes = { - RestConfiguration.class, RepositoryApplicationConfiguration.class, TestConfiguration.class }) + RestConfiguration.class, JpaRepositoryConfiguration.class, TestConfiguration.class }) @WebAppConfiguration @AutoConfigureMockMvc public abstract class AbstractRestIntegrationTest extends AbstractIntegrationTest { 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 46c46740b..2f637b336 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 @@ -400,8 +400,7 @@ public final class SpPermission { * context contains the anonymous role or the controller specific role * {@link SpringEvalExpressions#CONTROLLER_ROLE}. */ - public static final String IS_CONTROLLER = "hasAnyRole('" + CONTROLLER_ROLE_ANONYMOUS + "', '" + CONTROLLER_ROLE - + "')"; + public static final String IS_CONTROLLER = "hasAnyRole('" + CONTROLLER_ROLE_ANONYMOUS + "', '" + CONTROLLER_ROLE + "')"; /** * Spring security eval hasAuthority expression to check if spring * context contains {@link #IS_CONTROLLER} or