diff --git a/hawkbit-artifact/hawkbit-artifact-fs/src/test/java/org/eclipse/hawkbit/artifact/fs/FileArtifactStorageTest.java b/hawkbit-artifact/hawkbit-artifact-fs/src/test/java/org/eclipse/hawkbit/artifact/fs/FileArtifactStorageTest.java index dc4d5cc1a..9bb940f14 100644 --- a/hawkbit-artifact/hawkbit-artifact-fs/src/test/java/org/eclipse/hawkbit/artifact/fs/FileArtifactStorageTest.java +++ b/hawkbit-artifact/hawkbit-artifact-fs/src/test/java/org/eclipse/hawkbit/artifact/fs/FileArtifactStorageTest.java @@ -36,6 +36,8 @@ import org.junit.jupiter.api.Test; class FileArtifactStorageTest { private static final String TENANT = "test_tenant"; + @SuppressWarnings("java:S1068") // used for tests only, no need of secure random + private static final Random RND = new Random(); private static FileArtifactProperties artifactResourceProperties; private static FileArtifactStorage artifactFilesystemRepository; @@ -130,7 +132,7 @@ class FileArtifactStorageTest { private static byte[] randomBytes() { final byte[] randomBytes = new byte[20]; - new Random().nextBytes(randomBytes); + RND.nextBytes(randomBytes); return randomBytes; } 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 ae440db45..968b0a462 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 @@ -80,7 +80,6 @@ public abstract class AbstractDDiApiIntegrationTest extends AbstractRestIntegrat protected static final int ARTIFACT_SIZE = 5 * 1024; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final Random RND = new Random(); /** * Convert JSON to a CBOR equivalent. diff --git a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java index af103a22b..38818c228 100644 --- a/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java +++ b/hawkbit-dmf/hawkbit-dmf-amqp/src/main/java/org/eclipse/hawkbit/amqp/AmqpMessageHandlerService.java @@ -14,6 +14,7 @@ import static org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationPrope import java.io.Serializable; import java.net.URI; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; @@ -211,7 +212,7 @@ public class AmqpMessageHandlerService extends BaseAmqpService { return StringUtils.hasLength(message.getMessageProperties().getCorrelationId()); } - @SuppressWarnings("java:S2637" ) // java:S2637 - logAndThrowMessageError throws exception, i.e. doesn't return null + @SuppressWarnings("java:S2637") // java:S2637 - logAndThrowMessageError throws exception, i.e. doesn't return null private static @NotNull Status mapStatus(final Message message, final DmfActionUpdateStatus actionUpdateStatus, final Action action) { Status status = null; switch (actionUpdateStatus.getActionStatus()) { @@ -455,12 +456,16 @@ public class AmqpMessageHandlerService extends BaseAmqpService { } else if (actionUpdateStatus.getActionStatus() == DmfActionStatus.DENIED) { updatedAction = confirmationManagement.denyAction(action.getId(), actionUpdateStatus.getCode(), messages); } else { - final ActionStatusCreateBuilder actionStatus = ActionStatusCreate.builder() - .actionId(action.getId()).status(status).messages(messages); - Optional.ofNullable(actionUpdateStatus.getCode()).ifPresent(code -> { - actionStatus.code(code); - actionStatus.messages(List.of("Device reported status code: " + code)); - }); + final ActionStatusCreateBuilder actionStatus = ActionStatusCreate.builder().actionId(action.getId()) + .status(status); + Optional.ofNullable(actionUpdateStatus.getCode()).ifPresentOrElse( + code -> { + actionStatus.code(code); + final List withCodeReportedMessage = new ArrayList<>(messages); + withCodeReportedMessage.add("Device reported status code: " + code); + actionStatus.messages(withCodeReportedMessage); + }, + () -> actionStatus.messages(messages)); updatedAction = Status.CANCELED == status || Status.CANCEL_REJECTED == status ? controllerManagement.addCancelActionStatus(actionStatus.build()) : controllerManagement.addUpdateActionStatus(actionStatus.build()); diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTagResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTagResourceTest.java index 0bd9fe5d8..12ef50ce8 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTagResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtDistributionSetTagResourceTest.java @@ -24,7 +24,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Random; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.mgmt.rest.resource.util.ResourceUtility; @@ -54,7 +53,6 @@ import org.springframework.test.web.servlet.ResultActions; class MgmtDistributionSetTagResourceTest extends AbstractManagementApiIntegrationTest { private static final String DISTRIBUTIONSETTAGS_ROOT = "http://localhost" + MgmtRestConstants.DISTRIBUTIONSET_TAG_V1_REQUEST_MAPPING + "/"; - private static final Random RND = new Random(); /** * Verifies that a paged result list of DS tags reflects the content on the repository side. diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTagResourceTest.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTagResourceTest.java index 01ccb03f0..f14638419 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTagResourceTest.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/test/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetTagResourceTest.java @@ -25,7 +25,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Random; import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants; import org.eclipse.hawkbit.mgmt.rest.api.MgmtTargetTagRestApi; @@ -57,7 +56,6 @@ import org.springframework.test.web.servlet.ResultActions; public class MgmtTargetTagResourceTest extends AbstractManagementApiIntegrationTest { private static final String TARGETTAGS_ROOT = "http://localhost" + MgmtRestConstants.TARGET_TAG_V1_REQUEST_MAPPING + "/"; - private static final Random RND = new Random(); /** * Verifies that a paged result list of target tags reflects the content on the repository side. diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/PollingTime.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/PollingTime.java index 892fdead4..fbfec15f2 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/PollingTime.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/tenancy/configuration/PollingTime.java @@ -58,6 +58,7 @@ public class PollingTime { @Value public static class PollingInterval { + @SuppressWarnings("java:S1068") // used for random delay only, no need of secure random private static final Random RANDOM = new Random(); public static final String POLLING_INTERVALE_REGEX = "\\s{0,5}(?\\d{2}:[0-5]\\d:[0-5]\\d)\\s{0,5}(~(?\\d{1,2})%)?\\s{0,5}"; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ContextAwareTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ContextAwareTest.java index d2a384e98..94705aac0 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ContextAwareTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ContextAwareTest.java @@ -10,22 +10,31 @@ package org.eclipse.hawkbit.repository.jpa.acm; import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.hawkbit.security.SecurityContextSerializer.JSON_SERIALIZATION; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; +import java.util.List; +import java.util.concurrent.Callable; + +import lombok.SneakyThrows; import org.eclipse.hawkbit.ContextAware; +import org.eclipse.hawkbit.im.authentication.SpPermission; import org.eclipse.hawkbit.repository.autoassign.AutoAssignExecutor; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; import org.eclipse.hawkbit.repository.model.Rollout; import org.eclipse.hawkbit.repository.model.TargetFilterQuery; -import org.eclipse.hawkbit.security.SecurityContextSerializer; +import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.AuditorAware; import org.springframework.data.domain.Pageable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; @@ -35,14 +44,16 @@ import org.springframework.security.core.context.SecurityContextHolder; */ class ContextAwareTest extends AbstractJpaIntegrationTest { + private static final List AUTHORITIES = SpPermission.getAllAuthorities(); + @Autowired AutoAssignExecutor autoAssignExecutor; @Autowired ContextAware contextAware; - private static final SecurityContextSerializer SECURITY_CONTEXT_SERIALIZER = - SecurityContextSerializer.JAVA_SERIALIZATION; + @Autowired + AuditorAware auditorAware; @BeforeEach @AfterEach @@ -55,13 +66,12 @@ class ContextAwareTest extends AbstractJpaIntegrationTest { */ @Test void verifyAcmContextIsPersistedInCreatedRollout() { - final SecurityContext securityContext = SecurityContextHolder.getContext(); + final SecurityContext securityContext = createContext(0); assertThat(securityContext).isNotNull(); - final Rollout exampleRollout = testdataFactory.createRollout(); + final Rollout exampleRollout = runInContext(securityContext, testdataFactory::createRollout); assertThat(exampleRollout.getAccessControlContext()) - .hasValueSatisfying(ctx -> - assertThat(SECURITY_CONTEXT_SERIALIZER.deserialize(ctx)).isEqualTo(securityContext)); + .hasValueSatisfying(ctx -> assertEssentialEquals(JSON_SERIALIZATION.deserialize(ctx), securityContext)); } /** @@ -69,12 +79,11 @@ class ContextAwareTest extends AbstractJpaIntegrationTest { */ @Test void verifyContextIsReusedWhenHandlingRollout() { - final SecurityContext securityContext = SecurityContextHolder.getContext(); - assertThat(securityContext).isNotNull(); + final SecurityContext securityContext = createContext(1); // testdataFactory#createRollout will trigger a rollout handling - testdataFactory.createRollout(); - verify(contextAware).runInContext(eq(SECURITY_CONTEXT_SERIALIZER.serialize(securityContext)), any(Runnable.class)); + runInContext(securityContext, testdataFactory::createRollout); + verify(contextAware).runInContext(eq(JSON_SERIALIZATION.serialize(securityContext)), any(Runnable.class)); } /** @@ -82,13 +91,12 @@ class ContextAwareTest extends AbstractJpaIntegrationTest { */ @Test void verifyContextIsPersistedInActiveAutoAssignment() { - final SecurityContext securityContext = SecurityContextHolder.getContext(); - assertThat(securityContext).isNotNull(); + final SecurityContext securityContext = createContext(2); - final TargetFilterQuery targetFilterQuery = testdataFactory.createTargetFilterWithTargetsAndActiveAutoAssignment(); + final TargetFilterQuery targetFilterQuery = + runInContext(securityContext, testdataFactory::createTargetFilterWithTargetsAndActiveAutoAssignment); assertThat(targetFilterQuery.getAccessControlContext()) - .hasValueSatisfying(ctx -> - assertThat(SECURITY_CONTEXT_SERIALIZER.deserialize(ctx)).isEqualTo(securityContext)); + .hasValueSatisfying(ctx -> assertEssentialEquals(JSON_SERIALIZATION.deserialize(ctx), securityContext)); } /** @@ -96,12 +104,14 @@ class ContextAwareTest extends AbstractJpaIntegrationTest { */ @Test void verifyContextIsReusedWhenCheckingForAutoAssignmentAllTargets() { - final SecurityContext securityContext = SecurityContextHolder.getContext(); - assertThat(securityContext).isNotNull(); + final SecurityContext securityContext = createContext(3); - testdataFactory.createTargetFilterWithTargetsAndActiveAutoAssignment(); - autoAssignExecutor.checkAllTargets(); - verify(contextAware).runInContext(eq(SECURITY_CONTEXT_SERIALIZER.serialize(securityContext)), any(Runnable.class)); + runInContext(securityContext, testdataFactory::createTargetFilterWithTargetsAndActiveAutoAssignment); + runInContext(securityContext, () -> { + autoAssignExecutor.checkAllTargets(); + return null; + }); + verify(contextAware).runInContext(eq(JSON_SERIALIZATION.serialize(securityContext)), any(Runnable.class)); } /** @@ -109,12 +119,52 @@ class ContextAwareTest extends AbstractJpaIntegrationTest { */ @Test void verifyContextIsReusedWhenCheckingForAutoAssignmentSingleTarget() { - final SecurityContext securityContext = SecurityContextHolder.getContext(); + final SecurityContext securityContext = createContext(4); + + runInContext(securityContext, testdataFactory::createTargetFilterWithTargetsAndActiveAutoAssignment); + runInContext(securityContext, () -> { + autoAssignExecutor.checkSingleTarget(targetManagement.findAll(Pageable.ofSize(1)).getContent().get(0).getControllerId()); + return null; + }); + verify(contextAware).runInContext(eq(JSON_SERIALIZATION.serialize(securityContext)), any(Runnable.class)); + } + + private static SecurityContext createContext(final int testId) { + final SecurityContext securityContext = SecurityContextHolder.createEmptyContext(); + final UsernamePasswordAuthenticationToken userPassAuthentication = new UsernamePasswordAuthenticationToken( + "user", null, AUTHORITIES.stream().map(SimpleGrantedAuthority::new).toList()); + final TenantAwareAuthenticationDetails details = new TenantAwareAuthenticationDetails("my_tenant_" + testId, false); + userPassAuthentication.setDetails(details); + securityContext.setAuthentication(userPassAuthentication); + assertThat(securityContext).isNotNull(); - testdataFactory.createTargetFilterWithTargetsAndActiveAutoAssignment(); - autoAssignExecutor - .checkSingleTarget(targetManagement.findAll(Pageable.ofSize(1)).getContent().get(0).getControllerId()); - verify(contextAware).runInContext(eq(SECURITY_CONTEXT_SERIALIZER.serialize(securityContext)), any(Runnable.class)); + return securityContext; } -} + + @SneakyThrows + private T runInContext(final SecurityContext securityContext, final Callable runnable) { + SecurityContextHolder.setContext(securityContext); + try { + return runnable.call(); + } finally { + SecurityContextHolder.clearContext(); + } + } + + private void assertEssentialEquals(final SecurityContext sc1, final SecurityContext sc2) { + assertThat(auditor(sc1)).hasToString(auditor(sc2)); + assertThat(sc1.getAuthentication().getAuthorities()).isEqualTo(sc2.getAuthentication().getAuthorities()); + assertThat(sc1.getAuthentication().isAuthenticated()).isEqualTo(sc2.getAuthentication().isAuthenticated()); + assertThat(sc1.getAuthentication().getDetails()).isEqualTo(sc2.getAuthentication().getDetails()); + } + + private String auditor(final SecurityContext securityContext) { + SecurityContextHolder.setContext(securityContext); + try { + return auditorAware.getCurrentAuditor().orElseThrow(); + } finally { + SecurityContextHolder.clearContext(); + } + } +} \ No newline at end of file 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 787685331..4a4e8695f 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 @@ -13,7 +13,6 @@ import static org.assertj.core.api.Assertions.assertThat; import java.io.ByteArrayInputStream; import java.util.List; -import java.util.Random; import org.eclipse.hawkbit.im.authentication.SpRole; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; @@ -130,9 +129,8 @@ class SystemManagementTest extends AbstractJpaIntegrationTest { } private void createTestTenantsForSystemStatistics(final int tenants, final int artifactSize, final int targets, final int updates) { - final Random randomgen = new Random(); final byte[] random = new byte[artifactSize]; - randomgen.nextBytes(random); + RND.nextBytes(random); for (int i = 0; i < tenants; i++) { final String tenantname = "TENANT" + i; diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/TestConfiguration.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/TestConfiguration.java index 0a8601e72..ed53e2a59 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/TestConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/TestConfiguration.java @@ -146,7 +146,7 @@ public class TestConfiguration implements AsyncConfigurer { @Bean SecurityContextSerializer securityContextSerializer() { - return SecurityContextSerializer.JAVA_SERIALIZATION; + return SecurityContextSerializer.JSON_SERIALIZATION; } @Bean 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 e1420779a..fb7672f30 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 @@ -125,6 +125,7 @@ public abstract class AbstractIntegrationTest { protected static final URI LOCALHOST = URI.create("http://127.0.0.1"); protected static final int DEFAULT_TEST_WEIGHT = 500; + @SuppressWarnings("java:S1068") // used for tests only, no need of secure random protected static final Random RND = new Random(); /** diff --git a/hawkbit-security-core/pom.xml b/hawkbit-security-core/pom.xml index a36656dd5..f7f72f046 100644 --- a/hawkbit-security-core/pom.xml +++ b/hawkbit-security-core/pom.xml @@ -54,5 +54,10 @@ jakarta.servlet-api provided + + + com.fasterxml.jackson.core + jackson-databind + \ No newline at end of file diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextSerializer.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextSerializer.java index 4a1c1f6e5..427d8cdd8 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextSerializer.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SecurityContextSerializer.java @@ -14,13 +14,27 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.io.Serial; +import java.io.Serializable; import java.util.Base64; +import java.util.Collection; import java.util.Objects; +import java.util.stream.Stream; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.AccessLevel; +import lombok.Data; import lombok.NoArgsConstructor; +import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +// serializer for security contexts used for background tasks (processing auto assignments and rollouts) +// the user context is serialized on task creation and then is deserialized and applied when task is executed later public interface SecurityContextSerializer { /** @@ -29,8 +43,14 @@ public interface SecurityContextSerializer { */ SecurityContextSerializer NOP = new Nop(); /** - * Serializer the uses Java serialization of {@link java.io.Serializable} objects. - *

+ * Serializer the uses JSON serialization. + *

+ * Note that on deserialization this serialization does (if configured) fallback to {@link #JAVA_SERIALIZATION}. + */ + SecurityContextSerializer JSON_SERIALIZATION = new JsonSerialization(); + /** + * Serializer the uses Java serialization of {@link java.io.Serializable} objects (legacy, not recommended). + *

* Note that serialized via java serialization context might become unreadable if incompatible * changes are made to the object classes. */ @@ -68,11 +88,128 @@ public interface SecurityContextSerializer { } @Override - public SecurityContext deserialize(String securityContextString) { + public SecurityContext deserialize(final String securityContextString) { throw new UnsupportedOperationException(); } } + /** + * Implementation based on the java serialization. + */ + @NoArgsConstructor(access = AccessLevel.PRIVATE) + @SuppressWarnings("java:S112") // accepted + class JsonSerialization implements SecurityContextSerializer { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final boolean FALLBACK_TO_JAVA_SERIALIZATION = + !Boolean.getBoolean("hawkbit.security.contextSerializer.json.no-fallback-to-java"); + + @Override + public String serialize(final SecurityContext securityContext) { + Objects.requireNonNull(securityContext); + try { + return OBJECT_MAPPER.writeValueAsString(new SecCtxInfo(securityContext)); + } catch (final JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + @Override + public SecurityContext deserialize(final String securityContextString) { + Objects.requireNonNull(securityContextString); + final String securityContextTrimmed = securityContextString.trim(); + try { + // java serialization starts with {@link ObjectStreamConstants#STREAM_MAGIC} (0xAC, 0xED) bytes + // while trimmed json object starts with '{' + if (FALLBACK_TO_JAVA_SERIALIZATION && + (securityContextTrimmed.isEmpty() || securityContextTrimmed.charAt(0) != '{')) { + return JAVA_SERIALIZATION.deserialize(securityContextString); + } + + return OBJECT_MAPPER.readerFor(SecCtxInfo.class). readValue(securityContextTrimmed).toSecurityContext(); + } catch (final JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + // simplified info for the security context keeping just the basic info needed for background execution of + @NoArgsConstructor + @Data + private static class SecCtxInfo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String principal; + private String[] authorities; + private boolean authenticated; + private String tenant; + private boolean controller; + + SecCtxInfo(final SecurityContext securityContext) { + final Authentication authentication = securityContext.getAuthentication(); + if (authentication.getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareDetails) { + tenant = tenantAwareDetails.tenant(); + controller = tenantAwareDetails.controller(); + } else { + tenant = null; + controller = false; + } + // keep the auditor, ofr audit purposes, + // sets principal to the resolved auditor and then deserialized authentication will return it as principal + // since the class is not known to auditor aware - it shall used default - principal as auditor + principal = SpringSecurityAuditorAware.resolveAuditor(authentication); + authorities = authentication.getAuthorities().stream().map(Object::toString).toArray(String[]::new); + authenticated = authentication.isAuthenticated(); + } + + private SecurityContext toSecurityContext() { + final SecurityContext ctx = SecurityContextHolder.createEmptyContext(); + final Object details = tenant == null ? null : new TenantAwareAuthenticationDetails(tenant, controller); + final Collection grantedAuthorities = + Stream.of(authorities).map(SimpleGrantedAuthority::new).toList(); + ctx.setAuthentication(new Authentication() { + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public Collection getAuthorities() { + return grantedAuthorities; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } + + @Override + public Object getDetails() { + return details; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public void setAuthenticated(final boolean isAuthenticated) throws IllegalArgumentException { + throw new UnsupportedOperationException(); + } + + @Override + public String getName() { + return principal; + } + }); + return ctx; + } + } + } + /** * Implementation based on the java serialization. */ @@ -94,7 +231,7 @@ public interface SecurityContextSerializer { } @Override - public SecurityContext deserialize(String securityContextString) { + public SecurityContext deserialize(final String securityContextString) { Objects.requireNonNull(securityContextString); try (final ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(securityContextString)); final ObjectInputStream ois = new ObjectInputStream(bais)) { diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SpringSecurityAuditorAware.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SpringSecurityAuditorAware.java index ce5a07d31..fc41f7a7c 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SpringSecurityAuditorAware.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SpringSecurityAuditorAware.java @@ -11,6 +11,7 @@ package org.eclipse.hawkbit.security; import java.util.Optional; +import lombok.NonNull; import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails; import org.springframework.data.domain.AuditorAware; import org.springframework.security.core.Authentication; @@ -19,8 +20,7 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.oidc.user.OidcUser; /** - * Auditor class that allows BaseEntitys to insert current logged in user for - * repository changes. + * Auditor class that allows BaseEntity-s to insert current logged in user for repository changes. */ public class SpringSecurityAuditorAware implements AuditorAware { @@ -43,6 +43,7 @@ public class SpringSecurityAuditorAware implements AuditorAware { AUDITOR_OVERRIDE.remove(); } + @NonNull @Override public Optional getCurrentAuditor() { if (AUDITOR_OVERRIDE.get() != null) { @@ -55,10 +56,10 @@ public class SpringSecurityAuditorAware implements AuditorAware { return Optional.empty(); } - return Optional.ofNullable(getCurrentAuditor(authentication)); + return Optional.ofNullable(resolveAuditor(authentication)); } - protected String getCurrentAuditor(final Authentication authentication) { + public static String resolveAuditor(final Authentication authentication) { if (authentication.getDetails() instanceof TenantAwareAuthenticationDetails tenantAwareDetails && tenantAwareDetails.controller()) { return "CONTROLLER_PLUG_AND_PLAY"; } diff --git a/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/security/SecurityContextSerializerTest.java b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/security/SecurityContextSerializerTest.java new file mode 100644 index 000000000..10b3a152c --- /dev/null +++ b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/security/SecurityContextSerializerTest.java @@ -0,0 +1,91 @@ +/** + * 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.security; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.hawkbit.security.SecurityContextSerializer.JAVA_SERIALIZATION; +import static org.eclipse.hawkbit.security.SecurityContextSerializer.JSON_SERIALIZATION; + +import java.util.List; +import java.util.Map; + +import org.eclipse.hawkbit.im.authentication.SpPermission; +import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails; +import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +class SecurityContextSerializerTest { + + private static final List AUTHORITIES = SpPermission.getAllAuthorities(); + + @Test + void testJsonSerialization() { + final SecurityContext securityContext = SecurityContextHolder.getContext(); + final UsernamePasswordAuthenticationToken userPassAuthentication = new UsernamePasswordAuthenticationToken( + "user", null, AUTHORITIES.stream().map(SimpleGrantedAuthority::new).toList()); + final TenantAwareAuthenticationDetails details = new TenantAwareAuthenticationDetails("my_tenant", false); + userPassAuthentication.setDetails(details); + securityContext.setAuthentication(userPassAuthentication); + + final String serialized = JSON_SERIALIZATION.serialize(securityContext); + final SecurityContext deserialized = JSON_SERIALIZATION.deserialize(serialized); + final Authentication authentication = deserialized.getAuthentication(); + assertThat(authentication.getPrincipal()).hasToString("user"); + assertThat(authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList()).isEqualTo(AUTHORITIES); + assertThat(authentication.isAuthenticated()).isTrue(); + assertThat(authentication.getDetails()).isEqualTo(details); + } + + @Test + void testJsonSerializationSize() { + final SecurityContext securityContext = SecurityContextHolder.getContext(); + final UsernamePasswordAuthenticationToken userPassAuthentication = new UsernamePasswordAuthenticationToken( + "FirstName.FamilyName@domain1.domain0.com", + Map.of("should not be in" + bigString(10_000),"the output" + bigString(15_000)), + AUTHORITIES.stream().map(SimpleGrantedAuthority::new).toList()); + final TenantAwareAuthenticationDetails details = new TenantAwareAuthenticationDetails("my_test_enant", false); + userPassAuthentication.setDetails(details); + securityContext.setAuthentication(userPassAuthentication); + + final String serialized = JSON_SERIALIZATION.serialize(securityContext); + assertThat(serialized).hasSizeLessThan(4096); // ensure that it is not too big + } + + // test JSON serialization fallback to java serialization + @Test + void backwardCompatibilityOfJavaSerialization() { + final SecurityContext securityContext = SecurityContextHolder.getContext(); + securityContext.setAuthentication( + new UsernamePasswordAuthenticationToken("user", null, AUTHORITIES.stream().map(SimpleGrantedAuthority::new).toList())); + + final String newSerialized = JSON_SERIALIZATION.serialize(securityContext); + final String oldSerialized = JAVA_SERIALIZATION.serialize(securityContext); + + assertThat(oldSerialized).isNotEqualTo(newSerialized); + final Authentication deserializedOld = JSON_SERIALIZATION.deserialize(oldSerialized).getAuthentication(); + final Authentication deserializedNew = JSON_SERIALIZATION.deserialize(newSerialized).getAuthentication(); + assertThat(deserializedOld.getPrincipal()).hasToString(deserializedNew.getPrincipal().toString()); + assertThat(deserializedOld.getAuthorities()).isEqualTo(deserializedNew.getAuthorities()); + assertThat(deserializedOld.isAuthenticated()).isEqualTo(deserializedNew.isAuthenticated()); + } + + private static String bigString(final int length) { + final StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + sb.append((char) ('a' + i % 26)); + } + return sb.toString(); + } +}