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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}";
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -146,7 +146,7 @@ public class TestConfiguration implements AsyncConfigurer {
|
||||
|
||||
@Bean
|
||||
SecurityContextSerializer securityContextSerializer() {
|
||||
return SecurityContextSerializer.JAVA_SERIALIZATION;
|
||||
return SecurityContextSerializer.JSON_SERIALIZATION;
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -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();
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user