Implement JSON security context serializer (new default) - smaller info and human readable (#2652)

keeps backward compatibility by being able to fallback to JAVA_SERIALIZATION

+ fix DMF messages with status code

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-09-05 13:35:45 +03:00
committed by GitHub
parent e37fe75f15
commit 1f71e01318
14 changed files with 338 additions and 52 deletions

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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 -> {
final ActionStatusCreateBuilder actionStatus = ActionStatusCreate.builder().actionId(action.getId())
.status(status);
Optional.ofNullable(actionUpdateStatus.getCode()).ifPresentOrElse(
code -> {
actionStatus.code(code);
actionStatus.messages(List.of("Device reported status code: " + code));
});
final List<String> 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());

View File

@@ -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.

View File

@@ -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.

View File

@@ -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}(?<pollingInterval>\\d{2}:[0-5]\\d:[0-5]\\d)\\s{0,5}(~(?<deviationPercent>\\d{1,2})%)?\\s{0,5}";

View File

@@ -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<String> AUTHORITIES = SpPermission.getAllAuthorities();
@Autowired
AutoAssignExecutor autoAssignExecutor;
@Autowired
ContextAware contextAware;
private static final SecurityContextSerializer SECURITY_CONTEXT_SERIALIZER =
SecurityContextSerializer.JAVA_SERIALIZATION;
@Autowired
AuditorAware<String> 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();
runInContext(securityContext, testdataFactory::createTargetFilterWithTargetsAndActiveAutoAssignment);
runInContext(securityContext, () -> {
autoAssignExecutor.checkAllTargets();
verify(contextAware).runInContext(eq(SECURITY_CONTEXT_SERIALIZER.serialize(securityContext)), any(Runnable.class));
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> T runInContext(final SecurityContext securityContext, final Callable<T> 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();
}
}
}

View File

@@ -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;

View File

@@ -146,7 +146,7 @@ public class TestConfiguration implements AsyncConfigurer {
@Bean
SecurityContextSerializer securityContextSerializer() {
return SecurityContextSerializer.JAVA_SERIALIZATION;
return SecurityContextSerializer.JSON_SERIALIZATION;
}
@Bean

View File

@@ -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();
/**

View File

@@ -54,5 +54,10 @@
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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,7 +43,13 @@ public interface SecurityContextSerializer {
*/
SecurityContextSerializer NOP = new Nop();
/**
* Serializer the uses Java serialization of {@link java.io.Serializable} objects.
* Serializer the uses JSON serialization.
* <p/>
* 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).
* <p/>
* 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).<SecCtxInfo> 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<? extends GrantedAuthority> grantedAuthorities =
Stream.of(authorities).map(SimpleGrantedAuthority::new).toList();
ctx.setAuthentication(new Authentication() {
@Override
public Object getPrincipal() {
return principal;
}
@Override
public Collection<? extends GrantedAuthority> 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)) {

View File

@@ -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<String> {
@@ -43,6 +43,7 @@ public class SpringSecurityAuditorAware implements AuditorAware<String> {
AUDITOR_OVERRIDE.remove();
}
@NonNull
@Override
public Optional<String> getCurrentAuditor() {
if (AUDITOR_OVERRIDE.get() != null) {
@@ -55,10 +56,10 @@ public class SpringSecurityAuditorAware implements AuditorAware<String> {
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";
}

View File

@@ -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<String> 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();
}
}