diff --git a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetGroupResource.java b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetGroupResource.java index bbf6de297..c2ad39553 100644 --- a/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetGroupResource.java +++ b/hawkbit-mgmt/hawkbit-mgmt-resource/src/main/java/org/eclipse/hawkbit/mgmt/rest/resource/MgmtTargetGroupResource.java @@ -36,14 +36,16 @@ public class MgmtTargetGroupResource implements MgmtTargetGroupRestApi { private final TargetManagement targetManagement; private final TenantConfigHelper tenantConfigHelper; - public MgmtTargetGroupResource(final TargetManagement targetManagement, final SystemSecurityContext systemSecurityContext, - final TenantConfigurationManagement tenantConfigurationManagement) { + public MgmtTargetGroupResource( + final TargetManagement targetManagement, + final TenantConfigurationManagement tenantConfigurationManagement, final SystemSecurityContext systemSecurityContext) { this.targetManagement = targetManagement; this.tenantConfigHelper = TenantConfigHelper.usingContext(systemSecurityContext, tenantConfigurationManagement); } @Override - public ResponseEntity> getAssignedTargets(String group, int pagingOffsetParam, int pagingLimitParam, String sortParam) { + public ResponseEntity> getAssignedTargets( + final String group, final int pagingOffsetParam, final int pagingLimitParam, final String sortParam) { final Pageable pageable = PagingUtility.toPageable(pagingOffsetParam, pagingLimitParam, sanitizeTargetSortParam(sortParam)); final Page targets = targetManagement.findTargetsByGroup(group, false, pageable); @@ -53,7 +55,8 @@ public class MgmtTargetGroupResource implements MgmtTargetGroupRestApi { } @Override - public ResponseEntity> getAssignedTargetsWithSubgroups(String groupFilter, boolean subgroups, int pagingOffsetParam, int pagingLimitParam, String sortParam) { + public ResponseEntity> getAssignedTargetsWithSubgroups( + final String groupFilter, final boolean subgroups, final int pagingOffsetParam, final int pagingLimitParam, final String sortParam) { final Pageable pageable = PagingUtility.toPageable(pagingOffsetParam, pagingLimitParam, sanitizeTargetSortParam(sortParam)); final Page targets = targetManagement.findTargetsByGroup(groupFilter, subgroups, pageable); @@ -63,31 +66,29 @@ public class MgmtTargetGroupResource implements MgmtTargetGroupRestApi { } @Override - public ResponseEntity assignTargetsToGroup(String group, List controllerIds) { + public ResponseEntity assignTargetsToGroup(final String group, final List controllerIds) { return assignTargets(group, controllerIds); } @Override - public ResponseEntity assignTargetsToGroupWithSubgroups(String group, List controllerIds) { + public ResponseEntity assignTargetsToGroupWithSubgroups(final String group, final List controllerIds) { return assignTargets(group, controllerIds); } @Override - public ResponseEntity assignTargetsToGroupWithRsql(String group, String rsql) { - targetManagement.assignTargetGroupWithRsql(group, rsql); - return ResponseEntity.ok().build(); + public ResponseEntity assignTargetsToGroupWithRsql(final String group, final String rsql) { + return assignTargetsToGroupWithRsql0(group, rsql); } @Override - public ResponseEntity unassignTargetsFromGroup(List controllerIds) { + public ResponseEntity unassignTargetsFromGroup(final List controllerIds) { targetManagement.assignTargetsWithGroup(null, controllerIds); return ResponseEntity.ok().build(); } @Override - public ResponseEntity unassignTargetsFromGroupByRsql(String rsql) { - targetManagement.assignTargetGroupWithRsql(null, rsql); - return ResponseEntity.ok().build(); + public ResponseEntity unassignTargetsFromGroupByRsql(final String rsql) { + return assignTargetsToGroupWithRsql0(null, rsql); } @Override @@ -98,12 +99,16 @@ public class MgmtTargetGroupResource implements MgmtTargetGroupRestApi { @Override public ResponseEntity assignTargetsToGroup(final String group, final String rsql) { - targetManagement.assignTargetGroupWithRsql(group, rsql); - return ResponseEntity.ok().build(); + return assignTargetsToGroupWithRsql0(group, rsql); } private ResponseEntity assignTargets(final String group, final List controllerIds) { targetManagement.assignTargetsWithGroup(group, controllerIds); return ResponseEntity.ok().build(); } -} + + private ResponseEntity assignTargetsToGroupWithRsql0(final String group, final String rsql) { + targetManagement.assignTargetGroupWithRsql(group, rsql); + return ResponseEntity.ok().build(); + } +} \ No newline at end of file diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/Hierarchy.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/Hierarchy.java index 64f12d87f..cf9a68100 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/Hierarchy.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/Hierarchy.java @@ -9,6 +9,10 @@ */ package org.eclipse.hawkbit.im.authentication; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class Hierarchy { public static final String DEFAULT = diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java index 61193920d..167161f22 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java @@ -9,7 +9,11 @@ */ package org.eclipse.hawkbit.security; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.io.Serial; +import java.io.Serializable; import java.util.Collection; import java.util.List; import java.util.UUID; @@ -18,7 +22,6 @@ import java.util.concurrent.Callable; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.eclipse.hawkbit.im.authentication.SpRole; import org.eclipse.hawkbit.im.authentication.SpringEvalExpressions; @@ -166,7 +169,7 @@ public class SystemSecurityContext { * wraps the original authentication object. The wrapped object contains the necessary {@link SpRole#SYSTEM_ROLE} * which is allowed to execute all secured methods. */ - @Getter + @SuppressWarnings("java:S4275") // java:S4275 - intentionally returns the "hold" objects public static final class SystemCodeAuthentication implements Authentication { @Serial @@ -174,14 +177,14 @@ public class SystemSecurityContext { private static final List AUTHORITIES = List.of(new SimpleGrantedAuthority(SpRole.SYSTEM_ROLE)); - private final Object credentials; - private final Object details; - private final Object principal; + private final Holder credentials; + private final Holder details; + private final Holder principal; private SystemCodeAuthentication(final Authentication oldAuthentication) { - credentials = oldAuthentication != null ? oldAuthentication.getCredentials() : null; - details = oldAuthentication != null ? oldAuthentication.getDetails() : null; - principal = oldAuthentication != null ? oldAuthentication.getPrincipal() : null; + credentials = new Holder(oldAuthentication != null ? oldAuthentication.getCredentials() : null); + details = new Holder(oldAuthentication != null ? oldAuthentication.getDetails() : null); + principal = new Holder(oldAuthentication != null ? oldAuthentication.getPrincipal() : null); } @Override @@ -194,6 +197,21 @@ public class SystemSecurityContext { return AUTHORITIES; } + @Override + public Object getCredentials() { + return credentials.obj; + } + + @Override + public Object getDetails() { + return details.obj; + } + + @Override + public Object getPrincipal() { + return principal.obj; + } + @Override public boolean isAuthenticated() { return true; @@ -203,5 +221,28 @@ public class SystemSecurityContext { public void setAuthenticated(final boolean isAuthenticated) { throw new UnsupportedOperationException(); } + + // Serializable wrapper that ensures that the content will be serialized only if it is Serializable + private static class Holder implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Object obj; + + private Holder(final Object obj) { + this.obj = obj; + } + + @Serial + private void writeObject(final ObjectOutputStream oos) throws IOException { + oos.writeObject(obj instanceof Serializable ? obj : null); + } + + @Serial + private void readObject(final ObjectInputStream ois) throws IOException, ClassNotFoundException { + obj = ois.readObject(); + } + } } } \ No newline at end of file diff --git a/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/security/SystemCodeAuthenticationTest.java b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/security/SystemCodeAuthenticationTest.java new file mode 100644 index 000000000..a818e95ac --- /dev/null +++ b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/security/SystemCodeAuthenticationTest.java @@ -0,0 +1,123 @@ +/** + * 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 java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.eclipse.hawkbit.tenancy.TenantAware; +import org.junit.jupiter.api.Test; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +class SystemCodeAuthenticationTest { + + private static final SystemSecurityContext SYSTEM_SECURITY_CONTEXT = new SystemSecurityContext(new TenantAware() { + + @Override + public String getCurrentTenant() { + return "tenant"; + } + + @Override + public String getCurrentUsername() { + return "user"; + } + + @Override + public T runAsTenant(final String tenant, final TenantRunner tenantRunner) { + return tenantRunner.run(); + } + + @Override + public T runAsTenantAsUser(final String tenant, final String username, final TenantRunner tenantRunner) { + return tenantRunner.run(); + } + }); + + @Test + void testSerializationWithoutNull() { + final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("test", "pass", List.of(new SimpleGrantedAuthority("anonymous"))); + auth.setDetails("string details"); + test(auth); + } + + @Test + void testSerializationWithNullPrincipal() { + final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(null, "pass", List.of(new SimpleGrantedAuthority("anonymous"))); + auth.setDetails("string details"); + test(auth); + } + + @Test + void testSerializationWithNullCredentials() { + final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("test", null, List.of(new SimpleGrantedAuthority("anonymous"))); + auth.setDetails("string details"); + test(auth); + } + + @Test + void testSerializationWithNullDetails() { + final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("test", "pass", List.of(new SimpleGrantedAuthority("anonymous"))); + auth.setDetails(null); + test(auth); + } + + @Test + void testSerializationWitAllNull() { + final UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(null, null, List.of(new SimpleGrantedAuthority("anonymous"))); + auth.setDetails(null); + test(auth); + } + + private static void test(final UsernamePasswordAuthenticationToken auth) { + final SecurityContext sc = SecurityContextHolder.createEmptyContext(); + sc.setAuthentication(auth); + SecurityContextHolder.setContext(sc); + SYSTEM_SECURITY_CONTEXT.runAsSystemAsTenant(() -> { + final Authentication currentAuth = SecurityContextHolder.getContext().getAuthentication(); + Assertions.assertThat(currentAuth.getClass().getSimpleName()).isEqualTo("SystemCodeAuthentication"); + Assertions.assertThat(currentAuth.getPrincipal()).isEqualTo(auth.getPrincipal()); + Assertions.assertThat(currentAuth.getCredentials()).isEqualTo(auth.getCredentials()); + Assertions.assertThat(currentAuth.getAuthorities()).isEqualTo(List.of(new SimpleGrantedAuthority("ROLE_SYSTEM_CODE"))); + Assertions.assertThat(currentAuth.getDetails()).isEqualTo(auth.getDetails()); + + final Authentication serializedAndDeserializedAuth = serializeAndDeserialize(currentAuth); + Assertions.assertThat(serializedAndDeserializedAuth.getClass().getSimpleName()).isEqualTo("SystemCodeAuthentication"); + Assertions.assertThat(serializedAndDeserializedAuth.getPrincipal()).isEqualTo(auth.getPrincipal()); + Assertions.assertThat(serializedAndDeserializedAuth.getCredentials()).isEqualTo(auth.getCredentials()); + Assertions.assertThat(serializedAndDeserializedAuth.getAuthorities()).isEqualTo(List.of(new SimpleGrantedAuthority("ROLE_SYSTEM_CODE"))); + Assertions.assertThat(serializedAndDeserializedAuth.getDetails()).isEqualTo(auth.getDetails()); + return null; + }, "tenant"); + SecurityContextHolder.clearContext(); + } + + @SuppressWarnings("unchecked") + private static T serializeAndDeserialize(final T object) throws IOException, ClassNotFoundException { + try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + try (final ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(object); + oos.flush(); + } + try (final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) { + return (T) ois.readObject(); + } + } + } +} \ No newline at end of file