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:
@@ -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.
|
||||
* <p/>
|
||||
* 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