Fine-grained permissions (#2535)

* Fine-grained permissions

Adds support for permissions of type <permission>(/<rsql filter scope>)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>

* Apply review fixes

---------

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-07-10 13:51:49 +03:00
committed by GitHub
parent 7e8dd046e0
commit 21581c4ea4
69 changed files with 1492 additions and 1487 deletions

View File

@@ -36,7 +36,7 @@ import org.eclipse.hawkbit.repository.DistributionSetTypeManagement;
import org.eclipse.hawkbit.repository.EntityFactory;
import org.eclipse.hawkbit.repository.PropertiesQuotaManagement;
import org.eclipse.hawkbit.repository.QuotaManagement;
import org.eclipse.hawkbit.repository.RepositoryDefaultConfiguration;
import org.eclipse.hawkbit.repository.RepositoryConfiguration;
import org.eclipse.hawkbit.repository.RepositoryProperties;
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
import org.eclipse.hawkbit.repository.RolloutExecutor;
@@ -208,9 +208,9 @@ import org.springframework.validation.beanvalidation.MethodValidationPostProcess
@EnableRetry
@EntityScan("org.eclipse.hawkbit.repository.jpa.model")
@PropertySource("classpath:/hawkbit-jpa-defaults.properties")
@Import({ JpaConfiguration.class, RepositoryDefaultConfiguration.class, LockProperties.class, DataSourceAutoConfiguration.class, SystemManagementCacheKeyGenerator.class })
@Import({ JpaConfiguration.class, RepositoryConfiguration.class, LockProperties.class, DataSourceAutoConfiguration.class, SystemManagementCacheKeyGenerator.class })
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class RepositoryApplicationConfiguration {
public class JpaRepositoryConfiguration {
/**
* Defines the validation processor bean.

View File

@@ -0,0 +1,167 @@
/**
* 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.repository.jpa.acm;
import static org.eclipse.hawkbit.security.SecurityContextTenantAware.SYSTEM_USER;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.ContextAware;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.jpa.ql.EntityMatcher;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.ObjectUtils;
@Slf4j
public class DefaultAccessController<A extends Enum<A> & RsqlQueryField, T> implements AccessController<T> {
private final Class<A> rsqlQueryFieldType;
private final Map<Operation, List<String>> permissions = new EnumMap<>(Operation.class);
@Value("${hawkbit.jpa.security.default-access-controller.strict:false}")
private boolean strict;
private ContextAware contextAware;
private RoleHierarchy roleHierarchy;
public DefaultAccessController(final Class<A> rsqlQueryFieldType, final String... permissionTypes) {
if (ObjectUtils.isEmpty(permissionTypes)) {
throw new IllegalArgumentException("Permission types must not be empty");
}
this.rsqlQueryFieldType = rsqlQueryFieldType;
for (final Operation operation : Operation.values()) {
for (final String permissionType : permissionTypes) {
permissions.computeIfAbsent(operation, k -> new ArrayList<>()).add(operation.name() + "_" + permissionType.toUpperCase());
}
}
}
@Autowired
void setContextAware(final ContextAware contextAware) {
this.contextAware = contextAware;
}
@Autowired(required = false)
void setRoleHierarchy(final RoleHierarchy roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
@Override
public Optional<Specification<T>> getAccessRules(final Operation operation) {
if (contextAware.getCurrentTenant() != null && SYSTEM_USER.equals(contextAware.getCurrentUsername())) {
// as tenant, no restrictions
return Optional.empty();
}
return Optional.ofNullable(getScopes(operation)) // if get scopes returns null, no scopes return no spec - all entities are accessible
.map(scopes -> // to RSQL
scopes.size() == 1
? scopes.get(0) // single scope
: "(" + String.join(") or (", scopes) + ")") // join multiple scopes with 'or' - union
.map(scope -> RsqlUtility.getInstance().buildRsqlSpecification(scope, rsqlQueryFieldType));
}
@Override
public void assertOperationAllowed(final Operation operation, final T entity) throws InsufficientPermissionException {
if (contextAware.getCurrentTenant() != null && SYSTEM_USER.equals(contextAware.getCurrentUsername())) {
// as tenant, no restrictions
return;
}
final List<String> scopes = getScopes(operation);
if (scopes != null) {
for (final String scope : scopes) {
if (EntityMatcher.forRsql(scope).match(entity)) {
return; // at least one scope matches, operation is allowed
}
}
throw new InsufficientPermissionException(String.format("Operation '%s' is not allowed", operation));
} // else if scopes is null, no scopes are defined, so all entities are accessible
}
// returns null if ALL entities are accessible, otherwise returns a list of scopes
// throws InsufficientPermissionException if no matching authority found (should not happen - should be already checked with @PreAuthorize)
// java:S1168 - returns null with purpose to indicate no scopes, privately used with attention
// java:S1168 - better readable at one place
@SuppressWarnings({ "java:S1168", "java:S1168" })
private List<String> getScopes(final Operation operation) {
final List<String> operationPermissions = permissions.get(operation);
final List<String> scopes = SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.map(Permission::from)
.flatMap(permission -> roleHierarchy == null
? (operationPermissions.contains(permission.name()) ? Stream.of(permission) : Stream.empty())
: roleHierarchy.getReachableGrantedAuthorities(List.of(new SimpleGrantedAuthority(permission.name())))
.stream()
.map(GrantedAuthority::getAuthority)
.filter(operationPermissions::contains)
.map(reachableAuthority -> new Permission(reachableAuthority, permission.scope())))
.map(Permission::scope)
.distinct() // remove duplicates
.toList();
if (scopes.isEmpty()) {
// no matching authority found for the operation
// the needed permission should have already been checked with @PreAuthorize
// could happen, for instance, in controller management, that checks ROLE_CONTROLLER and on its behalf
// calls pure repository methods as privileged
if (strict) {
throw new InsufficientPermissionException(
String.format(
"No matching authority found for operation %s" +
" (expects %s, should not happen - shall have already been checked with @PreAuthorize)",
operation, operationPermissions));
} else {
// TODO - maybe in some future we could adapt permissions so controller roles to somehow apply what is needed
// and to do not "assume" and to throw exception always
log.debug(
"[{}] No matching authority found for operation {} (expects {}), they shall have already been checked with @PreAuthorize)",
rsqlQueryFieldType, operation, operationPermissions);
return null;
}
} else if (scopes.contains(null)) {
return null; // not scoped at all
} else {
return scopes;
}
}
private record Permission(String name, String scope) {
private static final Pattern PATTERN = Pattern.compile("^(?<name>[^/]+)(/(?<scope>.+))?$");
static Permission from(final String authority) {
final Matcher matcher = PATTERN.matcher(authority);
if (!matcher.matches()) {
throw new IllegalArgumentException("Invalid authority format: " + authority);
}
return from(matcher);
}
static Permission from(final Matcher matcher) {
return new Permission(matcher.group("name"), matcher.group("scope"));
}
}
}

View File

@@ -0,0 +1,43 @@
/**
* 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.repository.jpa.acm;
import org.eclipse.hawkbit.repository.DistributionSetFields;
import org.eclipse.hawkbit.repository.TargetFields;
import org.eclipse.hawkbit.repository.TargetTypeFields;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnProperty(name = "hawkbit.acm.access-controller.enabled", havingValue = "true", matchIfMissing = true)
public class DefaultAccessControllerConfiguration {
@Bean
@ConditionalOnProperty(name = "hawkbit.acm.access-controller.target.enabled", havingValue = "true", matchIfMissing = true)
AccessController<JpaTarget> targetAccessController() {
return new DefaultAccessController<>(TargetFields.class, "TARGET");
}
@Bean
@ConditionalOnProperty(name = "hawkbit.acm.access-controller.target-type.enabled", havingValue = "true", matchIfMissing = true)
AccessController<JpaTargetType> targetTypeAccessController() {
return new DefaultAccessController<>(TargetTypeFields.class, "TARGET", "TARGET_TYPE");
}
@Bean
@ConditionalOnProperty(name = "hawkbit.acm.access-controller.distribution-set.enabled", havingValue = "true", matchIfMissing = true)
AccessController<JpaDistributionSet> distributionSetAccessController() {
return new DefaultAccessController<>(DistributionSetFields.class, "REPOSITORY", "DISTRIBUTION_SET");
}
}

View File

@@ -29,6 +29,7 @@ import org.springframework.integration.support.MutableMessageHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.AbstractMessageConverter;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
@@ -37,28 +38,22 @@ import org.springframework.util.MimeTypeUtils;
/**
* Test the remote entity events.
*/
@TestPropertySource(properties = { "spring.cloud.bus.enabled=true" })
@SuppressWarnings("java:S6813") // constructor injects are not possible for test classes
public abstract class AbstractRemoteEventTest extends AbstractJpaIntegrationTest {
@Autowired
private BusProtoStuffMessageConverter busProtoStuffMessageConverter;
private AbstractMessageConverter jacksonMessageConverter;
@BeforeEach
public void setup() throws Exception {
final BusJacksonAutoConfiguration autoConfiguration = new BusJacksonAutoConfiguration();
this.jacksonMessageConverter = autoConfiguration.busJsonConverter(null);
ReflectionTestUtils.setField(jacksonMessageConverter, "packagesToScan",
new String[] { "org.eclipse.hawkbit.repository.event.remote",
ClassUtils.getPackageName(RemoteApplicationEvent.class) });
ReflectionTestUtils.setField(
jacksonMessageConverter, "packagesToScan",
new String[] { "org.eclipse.hawkbit.repository.event.remote", ClassUtils.getPackageName(RemoteApplicationEvent.class) });
((InitializingBean) jacksonMessageConverter).afterPropertiesSet();
}
protected Message<?> createMessageWithImmutableHeader(final TenantAwareEvent event) {
final Map<String, Object> headers = new LinkedHashMap<>();
return busProtoStuffMessageConverter.toMessage(event, new MessageHeaders(headers));
}
@SuppressWarnings("unchecked")
@@ -90,4 +85,4 @@ import org.springframework.util.MimeTypeUtils;
}
return null;
}
}
}

View File

@@ -27,8 +27,8 @@ import org.junit.jupiter.api.Test;
*/
class RemoteIdEventTest extends AbstractRemoteEventTest {
private static final long ENTITY_ID = 1L;
private static final String TENANT = "tenant";
private static final long ENTITY_ID = 1L;
private static final Class<? extends TenantAwareBaseEntity> ENTITY_CLASS = JpaAction.class;
private static final String CONTROLLER_ID = "controller911";
private static final String ADDRESS = "amqp://anyhost";
@@ -109,4 +109,4 @@ class RemoteIdEventTest extends AbstractRemoteEventTest {
// gets added because events inherit from of java.util.EventObject
assertThat(underTestCreatedEvent).usingRecursiveComparison().ignoringFields("source").isEqualTo(event);
}
}
}

View File

@@ -86,7 +86,6 @@ class RemoteTenantAwareEventTest extends AbstractRemoteEventTest {
*/
@Test
void testTargetAssignDistributionSetEvent() {
final DistributionSet dsA = testdataFactory.createDistributionSet("");
final JpaAction generateAction = new JpaAction();
@@ -114,7 +113,6 @@ class RemoteTenantAwareEventTest extends AbstractRemoteEventTest {
*/
@Test
void testCancelTargetAssignmentEvent() {
final DistributionSet dsA = testdataFactory.createDistributionSet("");
final JpaAction generateAction = new JpaAction();
@@ -145,9 +143,7 @@ class RemoteTenantAwareEventTest extends AbstractRemoteEventTest {
return generateAction;
}
private void assertTargetAssignDistributionSetEvent(final Action action,
final TargetAssignDistributionSetEvent underTest) {
private void assertTargetAssignDistributionSetEvent(final Action action, final TargetAssignDistributionSetEvent underTest) {
assertThat(underTest.getActions()).hasSize(1);
final ActionProperties actionProperties = underTest.getActions().get(action.getTarget().getControllerId());
assertThat(actionProperties).isNotNull();
@@ -162,4 +158,4 @@ class RemoteTenantAwareEventTest extends AbstractRemoteEventTest {
assertThat(actionProperties).usingRecursiveComparison().comparingOnlyFields().isEqualTo(new ActionProperties(action));
assertThat(underTest.getActionPropertiesForController(action.getTarget().getControllerId())).isPresent();
}
}
}

View File

@@ -70,4 +70,4 @@ public abstract class AbstractRemoteEntityEventTest<E> extends AbstractRemoteEve
}
protected abstract E createEntity();
}
}

View File

@@ -90,5 +90,4 @@ class ActionEventTest extends AbstractRemoteEntityEventTest<Action> {
generateAction.setWeight(1000);
return actionRepository.save(generateAction);
}
}
}

View File

@@ -30,7 +30,7 @@ class DistributionSetCreatedEventTest extends AbstractRemoteEntityEventTest<Dist
@Override
protected DistributionSet createEntity() {
return distributionSetManagement.create(entityFactory.distributionSet().create()
.name("incomplete").version("2").description("incomplete").type("os"));
return distributionSetManagement.create(
entityFactory.distributionSet().create().name("incomplete").version("2").description("incomplete").type("os"));
}
}

View File

@@ -35,7 +35,7 @@ class DistributionSetUpdatedEventTest extends AbstractRemoteEntityEventTest<Dist
@Override
protected DistributionSet createEntity() {
return distributionSetManagement.create(entityFactory.distributionSet().create()
.name("incomplete").version("2").description("incomplete").type("os"));
return distributionSetManagement.create(
entityFactory.distributionSet().create().name("incomplete").version("2").description("incomplete").type("os"));
}
}

View File

@@ -44,8 +44,9 @@ class RolloutEventTest extends AbstractRemoteEntityEventTest<Rollout> {
.type("os").modules(Collections.singletonList(module.getId())));
return rolloutManagement.create(
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").distributionSetId(ds), 5,
false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build());
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").distributionSetId(ds),
5,
false,
new RolloutGroupConditionBuilder().withDefaults().successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build());
}
}

View File

@@ -11,7 +11,7 @@ package org.eclipse.hawkbit.repository.event.remote.entity;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import org.eclipse.hawkbit.repository.model.DistributionSet;
@@ -81,13 +81,16 @@ class RolloutGroupEventTest extends AbstractRemoteEntityEventTest<RolloutGroup>
final SoftwareModule module = softwareModuleManagement.create(
entityFactory.softwareModule().create().name("swm").version("2").description("desc").type("os"));
final DistributionSet ds = distributionSetManagement
.create(entityFactory.distributionSet().create().name("complete").version("2").description("complete")
.type("os").modules(Collections.singletonList(module.getId())));
.create(entityFactory.distributionSet().create()
.name("complete").version("2")
.description("complete").type("os")
.modules(List.of(module.getId())));
final Rollout entity = rolloutManagement.create(
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").distributionSetId(ds), 5,
false, new RolloutGroupConditionBuilder().withDefaults()
.successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build());
entityFactory.rollout().create().name("exampleRollout").targetFilterQuery("controllerId==*").distributionSetId(ds),
5,
false,
new RolloutGroupConditionBuilder().withDefaults().successCondition(RolloutGroupSuccessCondition.THRESHOLD, "10").build());
return rolloutGroupManagement.findByRollout(entity.getId(), PAGE).getContent().get(0);
}

View File

@@ -40,5 +40,4 @@ class TargetEventTest extends AbstractRemoteEntityEventTest<Target> {
protected Target createEntity() {
return testdataFactory.createTarget("12345");
}
}
}

View File

@@ -40,5 +40,4 @@ class TargetTagEventTest extends AbstractRemoteEntityEventTest<TargetTag> {
protected TargetTag createEntity() {
return targetTagManagement.create(entityFactory.tag().create().name("tag1"));
}
}
}

View File

@@ -10,6 +10,8 @@
package org.eclipse.hawkbit.repository.jpa;
import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs;
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withUser;
import java.lang.reflect.Array;
import java.util.Collection;
@@ -62,7 +64,6 @@ import org.eclipse.hawkbit.repository.model.TargetTypeAssignmentResult;
import org.eclipse.hawkbit.repository.test.TestConfiguration;
import org.eclipse.hawkbit.repository.test.util.AbstractIntegrationTest;
import org.eclipse.hawkbit.repository.test.util.RolloutTestApprovalStrategy;
import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
@@ -73,7 +74,7 @@ import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@ContextConfiguration(classes = { RepositoryApplicationConfiguration.class, TestConfiguration.class })
@ContextConfiguration(classes = { JpaRepositoryConfiguration.class, TestConfiguration.class })
@TestPropertySource(locations = "classpath:/jpa-test.properties")
@SuppressWarnings("java:S6813") // constructor injects are not possible for test classes
public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest {
@@ -82,7 +83,9 @@ public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest
protected static final String NOT_EXIST_ID = "12345678990";
protected static final long NOT_EXIST_IDL = Long.parseLong(NOT_EXIST_ID);
private static final List<String> REPOSITORY_AND_TARGET_PERMISSIONS = List.of(SpPermission.READ_REPOSITORY, SpPermission.CREATE_REPOSITORY, SpPermission.UPDATE_REPOSITORY, SpPermission.DELETE_REPOSITORY, SpPermission.READ_TARGET, SpPermission.CREATE_TARGET, SpPermission.UPDATE_TARGET, SpPermission.DELETE_TARGET);
private static final List<String> REPOSITORY_AND_TARGET_PERMISSIONS = List.of(SpPermission.READ_REPOSITORY, SpPermission.CREATE_REPOSITORY,
SpPermission.UPDATE_REPOSITORY, SpPermission.DELETE_REPOSITORY, SpPermission.READ_TARGET, SpPermission.CREATE_TARGET,
SpPermission.UPDATE_TARGET, SpPermission.DELETE_TARGET);
@PersistenceContext
protected EntityManager entityManager;
@@ -230,33 +233,33 @@ public abstract class AbstractJpaIntegrationTest extends AbstractIntegrationTest
/**
* Asserts that the given callable throws an InsufficientPermissionException.
*
* @param callable the callable to call
* @param requiredPermissions required permissions for the callable
* @param insufficientPermissions can be null, if null, it will be resolved automatically. But in some cases (e.g. @PreAuthorized Permissions with OR, it is safer to pass directly the insufficient permissions)
*/
@SneakyThrows
protected void assertPermissions(final Callable<?> callable, final List<String> requiredPermissions, final List<String> insufficientPermissions) {
protected void assertPermissions(final Callable<?> callable, final List<String> requiredPermissions,
final List<String> insufficientPermissions) {
// if READ_PERMISSION is required and required permissions are multiple, give only READ_PERMISSION to eliminate internal read_permission check failure that would confuse the actual test
final List<String> resolvedInsufficientPermissions = insufficientPermissions != null ? insufficientPermissions :
requiredPermissions.contains(SpPermission.READ_REPOSITORY) && requiredPermissions.size() > 1 ?
List.of(SpPermission.READ_REPOSITORY) : REPOSITORY_AND_TARGET_PERMISSIONS.stream()
.filter(p -> !requiredPermissions.contains(p)).toList();
final List<String> resolvedInsufficientPermissions = insufficientPermissions != null
? insufficientPermissions
: requiredPermissions.contains(SpPermission.READ_REPOSITORY) && requiredPermissions.size() > 1
? List.of(SpPermission.READ_REPOSITORY)
: REPOSITORY_AND_TARGET_PERMISSIONS.stream().filter(p -> !requiredPermissions.contains(p)).toList();
// check if the user has the correct permissions
SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("user_with_permissions", requiredPermissions.toArray(new String[0])), () -> {
runAs(withUser("user_with_permissions", requiredPermissions.toArray(new String[0])), () -> {
assertPermissionWorks(callable);
log.info("assertPermissionWorks Passed");
return null;
});
// check if the user has the insufficient permissions
SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("user_without_permissions", resolvedInsufficientPermissions.toArray(new String[0])), () -> {
runAs(withUser("user_without_permissions", resolvedInsufficientPermissions.toArray(new String[0])), () -> {
assertInsufficientPermission(callable);
log.info("assertInsufficientPermission Passed");
return null;
});
}
/**
* Asserts that the given callable throws an InsufficientPermissionException.
* If callable succeeds without any exception or exception other than InsufficientPermissionException, it will be considered as an assert failure.

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 Bosch.IO GmbH and others
* 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
@@ -7,7 +7,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.acm.context;
package org.eclipse.hawkbit.repository.jpa.acm;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;

View File

@@ -0,0 +1,274 @@
/**
* 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.repository.jpa.acm;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_REPOSITORY;
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_TARGET;
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_REPOSITORY;
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_TARGET;
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs;
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withUser;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.builder.AutoAssignDistributionSetUpdate;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetFilter;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Pageable;
import org.springframework.test.context.ContextConfiguration;
/**
* Feature: Component Tests - Access Control<br/>
* Story: Test Distribution Set Access Controller
*/
@ContextConfiguration(classes = { DefaultAccessControllerConfiguration.class })
class DistributionSetAccessControllerTest extends AbstractJpaIntegrationTest {
/**
* Verifies read access rules for distribution sets
*/
@Test
void verifyDistributionSetReadOperations() {
final DistributionSet permitted = testdataFactory.createDistributionSet();
final DistributionSet hidden = testdataFactory.createDistributionSet();
final Action permittedAction = testdataFactory.performAssignment(permitted);
final Action hiddenAction = testdataFactory.performAssignment(hidden);
runAs(withUser("user",
READ_REPOSITORY + "/id==" + permitted.getId(),
READ_TARGET +"/controllerId==" + permittedAction.getTarget().getControllerId()), () -> {
final Long permittedActionId = permitted.getId();
// verify distributionSetManagement#findAll
assertThat(distributionSetManagement.findAll(Pageable.unpaged()).get().map(Identifiable::getId).toList())
.containsOnly(permittedActionId);
// verify distributionSetManagement#findByRsql
assertThat(distributionSetManagement.findByRsql("name==*", Pageable.unpaged()).get().map(Identifiable::getId)
.toList()).containsOnly(permittedActionId);
// verify distributionSetManagement#findByCompleted
assertThat(distributionSetManagement.findByCompleted(true, Pageable.unpaged()).get().map(Identifiable::getId)
.toList()).containsOnly(permittedActionId);
// verify distributionSetManagement#findByDistributionSetFilter
assertThat(distributionSetManagement
.findByDistributionSetFilter(DistributionSetFilter.builder().isDeleted(false).build(), Pageable.unpaged())
.get().map(Identifiable::getId).toList()).containsOnly(permittedActionId);
// verify distributionSetManagement#get
assertThat(distributionSetManagement.get(permittedActionId)).isPresent();
final Long hiddenId = hidden.getId();
assertThat(distributionSetManagement.get(hiddenId)).isEmpty();
// verify distributionSetManagement#getWithDetails
assertThat(distributionSetManagement.getWithDetails(permittedActionId)).isPresent();
assertThat(distributionSetManagement.getWithDetails(hiddenId)).isEmpty();
// verify distributionSetManagement#get
assertThat(distributionSetManagement.getValid(permittedActionId).getId()).isEqualTo(permittedActionId);
assertThatThrownBy(() -> distributionSetManagement.getValid(hiddenId))
.as("Distribution set should not be found.").isInstanceOf(EntityNotFoundException.class);
// verify distributionSetManagement#get
final List<Long> allActionIds = Arrays.asList(permittedActionId, hiddenId);
assertThatThrownBy(() -> distributionSetManagement.get(allActionIds))
.as("Fail if request hidden.").isInstanceOf(EntityNotFoundException.class);
// verify distributionSetManagement#getByNameAndVersion
assertThat(distributionSetManagement.findByNameAndVersion(permitted.getName(), permitted.getVersion())).isPresent();
assertThat(distributionSetManagement.findByNameAndVersion(hidden.getName(), hidden.getVersion())).isEmpty();
// verify distributionSetManagement#getByAction
assertThat(distributionSetManagement.findByAction(permittedAction.getId())).isPresent();
final Long hiddenActionId = hiddenAction.getId();
assertThatThrownBy(() -> distributionSetManagement.findByAction(hiddenActionId))
.as("Action is hidden.").isInstanceOf(InsufficientPermissionException.class);
});
}
/**
* Verifies read access rules for distribution sets
*/
@Test
void verifyDistributionSetUpdates() {
final DistributionSet permitted = testdataFactory.createDistributionSet();
final String mdPresetKey = "metadata.preset";
final String mdPresetValue = "presetValue";
distributionSetManagement.createMetadata(permitted.getId(), Map.of(mdPresetKey, mdPresetValue));
final DistributionSet readOnly = testdataFactory.createDistributionSet();
distributionSetManagement.createMetadata(readOnly.getId(), Map.of(mdPresetKey, mdPresetValue));
final DistributionSet hidden = testdataFactory.createDistributionSet();
distributionSetManagement.createMetadata(hidden.getId(), Map.of(mdPresetKey, mdPresetValue));
final SoftwareModule swModule = testdataFactory.createSoftwareModuleOs();
runAs(withUser("user",
READ_REPOSITORY + "/id==" + permitted.getId() + " or id==" + readOnly.getId(),
UPDATE_REPOSITORY + "/id==" + permitted.getId()), () -> {
// verify distributionSetManagement#assignSoftwareModules
final List<Long> singleModuleIdList = Collections.singletonList(swModule.getId());
assertThat(distributionSetManagement.assignSoftwareModules(permitted.getId(), singleModuleIdList))
.satisfies(ds -> assertThat(ds.getModules().stream().map(Identifiable::getId).toList()).contains(swModule.getId()));
final Long readOnlyId = readOnly.getId();
assertThatThrownBy(() -> distributionSetManagement.assignSoftwareModules(readOnlyId, singleModuleIdList))
.as("Distribution set not allowed to me modified.")
.isInstanceOf(InsufficientPermissionException.class);
final Long hiddenId = hidden.getId();
assertThatThrownBy(() -> distributionSetManagement.assignSoftwareModules(hiddenId, singleModuleIdList))
.as("Distribution set should not be visible.")
.isInstanceOf(EntityNotFoundException.class);
final Map<String, String> metadata = Map.of("test.create", mdPresetValue);
// verify distributionSetManagement#createMetaData
distributionSetManagement.createMetadata(permitted.getId(), metadata);
assertThatThrownBy(() -> distributionSetManagement.createMetadata(readOnlyId, metadata))
.as("Distribution set not allowed to be modified.")
.isInstanceOf(InsufficientPermissionException.class);
assertThatThrownBy(() -> distributionSetManagement.createMetadata(hiddenId, metadata))
.as("Distribution set should not be visible.")
.isInstanceOf(EntityNotFoundException.class);
// verify distributionSetManagement#updateMetaData
final String newValue = "newValue";
distributionSetManagement.updateMetadata(permitted.getId(), mdPresetKey, newValue);
assertThatThrownBy(() -> distributionSetManagement.updateMetadata(readOnlyId, mdPresetKey, newValue))
.as("Distribution set not allowed to me modified.")
.isInstanceOf(InsufficientPermissionException.class);
assertThatThrownBy(() -> distributionSetManagement.updateMetadata(hiddenId, mdPresetKey, newValue))
.as("Distribution set should not be visible.")
.isInstanceOf(EntityNotFoundException.class);
// verify distributionSetManagement#deleteMetaData
final String metadataKey = metadata.entrySet().stream().findAny().get().getKey();
distributionSetManagement.deleteMetadata(permitted.getId(), metadataKey);
assertThatThrownBy(() -> distributionSetManagement.deleteMetadata(readOnlyId, mdPresetKey))
.as("Distribution set not allowed to me modified.")
.isInstanceOf(InsufficientPermissionException.class);
assertThatThrownBy(() -> distributionSetManagement.deleteMetadata(hiddenId, mdPresetKey))
.as("Distribution set should not be visible.")
.isInstanceOf(EntityNotFoundException.class);
});
}
@Test
void verifyTagFilteringAndManagement() {
final DistributionSet permitted = testdataFactory.createDistributionSet();
final DistributionSet readOnly = testdataFactory.createDistributionSet();
final DistributionSet hidden = testdataFactory.createDistributionSet();
final Long dsTagId = distributionSetTagManagement.create(entityFactory.tag().create().name("dsTag")).getId();
final Long dsTag2Id = distributionSetTagManagement.create(entityFactory.tag().create().name("dsTag2")).getId();
// perform tag assignment before setting access rules
distributionSetManagement.assignTag(Arrays.asList(permitted.getId(), readOnly.getId(), hidden.getId()), dsTagId);
runAs(withUser("user",
READ_REPOSITORY + "/id==" + permitted.getId() + " or id==" + readOnly.getId(),
UPDATE_REPOSITORY + "/id==" + permitted.getId()), () -> {
assertThat(distributionSetManagement.findByTag(dsTagId, Pageable.unpaged()).get().map(Identifiable::getId)
.toList()).containsOnly(permitted.getId(), readOnly.getId());
assertThat(distributionSetManagement.findByRsqlAndTag("name==*", dsTagId, Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permitted.getId(), readOnly.getId());
// verify distributionSetManagement#unassignTag on permitted target
assertThat(distributionSetManagement
.unassignTag(Collections.singletonList(permitted.getId()), dsTagId))
.size()
.isEqualTo(1);
// verify distributionSetManagement#assignTag on permitted target
assertThat(distributionSetManagement.assignTag(Collections.singletonList(permitted.getId()), dsTagId))
.hasSize(1);
// verify distributionSetManagement#unAssignTag on permitted target
assertThat(distributionSetManagement.unassignTag(List.of(permitted.getId()), dsTagId)
.get(0).getId())
.isEqualTo(permitted.getId());
// assignment is denied for readOnlyTarget (read, but no update permissions)
final List<Long> readOblyList = Collections.singletonList(readOnly.getId());
assertThatThrownBy(() ->
distributionSetManagement.unassignTag(readOblyList, dsTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(InsufficientPermissionException.class);
// assignment is denied for readOnlyTarget (read, but no update permissions)
// dsTag2- since - it is tagged with dsTag and won't do anything if assigning dsTag
assertThatThrownBy(() -> distributionSetManagement.assignTag(readOblyList, dsTag2Id))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(InsufficientPermissionException.class);
// assignment is denied for hiddenTarget since it's hidden
final List<Long> hiddenList = Collections.singletonList(hidden.getId());
assertThatThrownBy(() -> distributionSetManagement.unassignTag(hiddenList, dsTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(EntityNotFoundException.class);
// assignment is denied for hiddenTarget since it's hidden
assertThatThrownBy(() -> distributionSetManagement.assignTag(hiddenList, dsTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(EntityNotFoundException.class);
// assignment is denied for hiddenTarget since it's hidden
final List<Long> hiddenIdList = List.of(hidden.getId());
assertThatThrownBy(() -> distributionSetManagement.unassignTag(hiddenIdList, dsTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(EntityNotFoundException.class);
});
}
@Test
void verifyAutoAssignmentUsage() {
final DistributionSet permitted = testdataFactory.createDistributionSet();
final DistributionSet readOnly = testdataFactory.createDistributionSet();
final DistributionSet hidden = testdataFactory.createDistributionSet();
// has to lock them, otherwise implicit lock shall be made which require DistributionSet update permissions
distributionSetManagement.lock(permitted.getId());
distributionSetManagement.lock(readOnly.getId());
distributionSetManagement.lock(hidden.getId());
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
.create(entityFactory.targetFilterQuery().create().name("test").query("id==*"));
runAs(withUser("user",
READ_REPOSITORY + "/id==" + permitted.getId() + " or id==" + readOnly.getId(),
UPDATE_REPOSITORY + "/id==" + permitted.getId(),
// read / update target needed to update target filter query
READ_TARGET, UPDATE_TARGET), () -> {
assertThat(targetFilterQueryManagement
.updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()).ds(permitted.getId())
.actionType(Action.ActionType.FORCED).confirmationRequired(false))
.getAutoAssignDistributionSet().getId()).isEqualTo(permitted.getId());
targetFilterQueryManagement
.updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId())
.ds(readOnly.getId()).actionType(Action.ActionType.FORCED).confirmationRequired(false))
.getAutoAssignDistributionSet().getId();
final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate = new AutoAssignDistributionSetUpdate(
targetFilterQuery.getId())
.ds(hidden.getId()).actionType(Action.ActionType.FORCED).confirmationRequired(false);
assertThatThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(autoAssignDistributionSetUpdate))
.isInstanceOf(EntityNotFoundException.class);
});
}
}

View File

@@ -0,0 +1,340 @@
/**
* 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.repository.jpa.acm;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.eclipse.hawkbit.im.authentication.SpPermission.CREATE_ROLLOUT;
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_REPOSITORY;
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_ROLLOUT;
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_TARGET;
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_TARGET;
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs;
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withUser;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.eclipse.hawkbit.repository.FilterParams;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.autoassign.AutoAssignChecker;
import org.eclipse.hawkbit.repository.model.Action.ActionType;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.test.context.ContextConfiguration;
/**
* Feature: Component Tests - Access Control<br/>
* Story: Test Target Access Controller
*/
@ContextConfiguration(classes = { DefaultAccessControllerConfiguration.class })
class TargetAccessControllerTest extends AbstractJpaIntegrationTest {
@Autowired
AutoAssignChecker autoAssignChecker;
/**
* Verifies read access rules for targets
*/
@Test
void verifyTargetReadOperations() {
final Target permittedTarget = targetManagement
.create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED));
final Target hiddenTarget = targetManagement
.create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED));
runAs(withUser("user", READ_TARGET + "/controllerId==" + permittedTarget.getControllerId()), () -> {
// verify targetManagement#findAll
assertThat(targetManagement.findAll(Pageable.unpaged()).get().map(Identifiable::getId).toList())
.containsOnly(permittedTarget.getId());
// verify targetManagement#findByRsql
assertThat(targetManagement.findByRsql("id==*", Pageable.unpaged()).get().map(Identifiable::getId).toList())
.containsOnly(permittedTarget.getId());
// verify targetManagement#findByUpdateStatus
assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.REGISTERED, Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId());
// verify targetManagement#getByControllerID
assertThat(targetManagement.getByControllerID(permittedTarget.getControllerId())).isPresent();
final String hiddenTargetControllerId = hiddenTarget.getControllerId();
assertThatThrownBy(() -> targetManagement.getByControllerID(hiddenTargetControllerId))
.as("Missing read permissions for hidden target.")
.isInstanceOf(InsufficientPermissionException.class);
// verify targetManagement#getByControllerID
assertThat(targetManagement
.getByControllerID(Arrays.asList(permittedTarget.getControllerId(), hiddenTargetControllerId))
.stream().map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId());
// verify targetManagement#get
assertThat(targetManagement.get(permittedTarget.getId())).isPresent();
assertThat(targetManagement.get(hiddenTarget.getId())).isEmpty();
// verify targetManagement#get
assertThat(targetManagement.get(Arrays.asList(permittedTarget.getId(), hiddenTarget.getId())).stream()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId());
// verify targetManagement#getControllerAttributes
assertThat(targetManagement.getControllerAttributes(permittedTarget.getControllerId())).isEmpty();
assertThatThrownBy(() -> targetManagement.getControllerAttributes(hiddenTargetControllerId))
.as("Target should not be found.")
.isInstanceOf(InsufficientPermissionException.class);
});
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
.create(entityFactory.targetFilterQuery().create().name("test").query("id==*"));
runAs(withUser("user", READ_TARGET + "/controllerId==" + permittedTarget.getControllerId()), () -> {
// verify targetManagement#findByTargetFilterQuery
assertThat(targetManagement.findByTargetFilterQuery(targetFilterQuery.getId(), Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId());
// verify targetManagement#findByTargetFilterQuery (used by UI)
assertThat(targetManagement.findByFilters(new FilterParams(null, null, null, null), Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId());
});
}
@Test
void verifyTagFilteringAndManagement() {
final Target permittedTarget = targetManagement
.create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED));
final Target readOnlyTarget = targetManagement
.create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED));
final String readOnlyTargetControllerId = readOnlyTarget.getControllerId();
final Target hiddenTarget = targetManagement
.create(entityFactory.target().create().controllerId("device03").status(TargetUpdateStatus.REGISTERED));
final Long myTagId = targetTagManagement.create(entityFactory.tag().create().name("myTag")).getId();
// perform tag assignment before setting access rules
targetManagement.assignTag(
Arrays.asList(permittedTarget.getControllerId(), readOnlyTargetControllerId, hiddenTarget.getControllerId()), myTagId);
runAs(withUser("user",
READ_TARGET + "/controllerId==" + permittedTarget.getControllerId() + " or controllerId==" + readOnlyTargetControllerId,
UPDATE_TARGET + "/controllerId==" + permittedTarget.getControllerId(),
READ_REPOSITORY),
() -> {
// verify targetManagement#findByTag
assertThat(
targetManagement.findByTag(myTagId, Pageable.unpaged()).get().map(Identifiable::getId).toList())
.containsOnly(permittedTarget.getId(), readOnlyTarget.getId());
// verify targetManagement#findByRsqlAndTag
assertThat(targetManagement.findByRsqlAndTag("id==*", myTagId, Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId(), readOnlyTarget.getId());
// verify targetManagement#assignTag on permitted target
assertThat(targetManagement.assignTag(Collections.singletonList(permittedTarget.getControllerId()), myTagId)).hasSize(1);
// verify targetManagement#unassignTag on permitted target
assertThat(targetManagement.unassignTag(Collections.singletonList(permittedTarget.getControllerId()), myTagId)).hasSize(1);
// verify targetManagement#assignTag on permitted target
assertThat(targetManagement.assignTag(Collections.singletonList(permittedTarget.getControllerId()), myTagId))
.hasSize(1);
// verify targetManagement#unAssignTag on permitted target
assertThat(targetManagement.unassignTag(List.of(permittedTarget.getControllerId()), myTagId).get(0).getControllerId())
.isEqualTo(permittedTarget.getControllerId());
// assignment is denied for readOnlyTarget (read, but no update permissions)
final List<String> readTargetControllerIdList = Collections.singletonList(readOnlyTargetControllerId);
assertThatThrownBy(() -> targetManagement.assignTag(readTargetControllerIdList, myTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOfAny(InsufficientPermissionException.class);
// assignment is denied for readOnlyTarget (read, but no update permissions)
final List<String> readOnlyTargetControllerIdList = List.of(readOnlyTargetControllerId);
assertThatThrownBy(() -> targetManagement.unassignTag(readOnlyTargetControllerIdList, myTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(InsufficientPermissionException.class);
// assignment is denied for hiddenTarget since it's hidden
final List<String> hiddenTargetControllerIdList = Collections.singletonList(hiddenTarget.getControllerId());
assertThatThrownBy(() -> targetManagement.assignTag(hiddenTargetControllerIdList, myTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(InsufficientPermissionException.class);
// assignment is denied for hiddenTarget since it's hidden
assertThatThrownBy(() -> targetManagement.assignTag(hiddenTargetControllerIdList, myTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(InsufficientPermissionException.class);
// assignment is denied for hiddenTarget since it's hidden
assertThatThrownBy(() -> targetManagement.unassignTag(hiddenTargetControllerIdList, myTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(InsufficientPermissionException.class);
});
}
/**
* Verifies rules for target assignment
*/
@Test
void verifyTargetAssignment() {
final Long dsId = testdataFactory.createDistributionSet("myDs").getId();
distributionSetManagement.lock(dsId);
final Target permittedTarget = targetManagement
.create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED));
final String hiddenTargetControllerId = targetManagement
.create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED))
.getControllerId();
runAs(withUser("user", READ_TARGET + "/controllerId==" + permittedTarget.getControllerId()), () ->
// verify targetManagement#findByUpdateStatus before assignment
assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.REGISTERED, Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId()));
runAs(withUser("user",
READ_TARGET + "/controllerId==" + permittedTarget.getControllerId(),
UPDATE_TARGET + "/controllerId==" + permittedTarget.getControllerId(),
READ_REPOSITORY), () -> {
assertThat(assignDistributionSet(dsId, permittedTarget.getControllerId()).getAssigned()).isEqualTo(1);
// assigning of not allowed target behaves as not found
assertThatThrownBy(() -> assignDistributionSet(dsId, hiddenTargetControllerId)).isInstanceOf(AssertionError.class);
// verify targetManagement#findByUpdateStatus(REGISTERED) after assignment
assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.REGISTERED, Pageable.unpaged())
.getTotalElements()).isZero();
// verify targetManagement#findByUpdateStatus(PENDING) after assignment
assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.PENDING, Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId());
});
}
/**
* Verifies rules for target assignment
*/
@Test
void verifyTargetAssignmentOnNonUpdatableTarget() {
final Long firstDsId = testdataFactory.createDistributionSet("myDs").getId();
distributionSetManagement.lock(firstDsId);
final DistributionSet secondDs = testdataFactory.createDistributionSet("anotherDs");
distributionSetManagement.lock(secondDs.getId());
final Target manageableTarget = targetManagement
.create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED));
final Target readOnlyTarget = targetManagement
.create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED));
runAs(withUser("user",
READ_TARGET + "/controllerId==" + manageableTarget.getControllerId() + " or controllerId==" + readOnlyTarget.getControllerId(),
UPDATE_TARGET + "/controllerId==" + manageableTarget.getControllerId(),
READ_REPOSITORY), () -> {
// assignment is permitted for manageableTarget
assertThat(assignDistributionSet(firstDsId, manageableTarget.getControllerId()).getAssigned()).isEqualTo(1);
// assignment is denied for readOnlyTarget (read, but no update permissions)
final var readOnlyTargetControllerId = readOnlyTarget.getControllerId();
assertThatThrownBy(() -> assignDistributionSet(firstDsId, readOnlyTargetControllerId)).isInstanceOf(AssertionError.class);
// bunch assignment skips denied since at least one target without update permissions is present
assertThat(assignDistributionSet(secondDs.getId(),
Arrays.asList(readOnlyTargetControllerId, manageableTarget.getControllerId()),
ActionType.FORCED).getAssigned()).isEqualTo(1);
});
}
/**
* Verifies only manageable targets are part of the rollout
*/
@Test
void verifyRolloutTargetScope() {
final DistributionSet ds = testdataFactory.createDistributionSet("myDs");
distributionSetManagement.lock(ds.getId());
final String[] updateTargetControllerIds = { "update1", "update2", "update3" };
final List<Target> updateTargets = testdataFactory.createTargets(updateTargetControllerIds);
final String[] readTargetControllerIds = { "read1", "read2", "read3", "read4" };
final List<Target> readTargets = testdataFactory.createTargets(readTargetControllerIds);
final List<Target> hiddenTargets = testdataFactory.createTargets("hidden1", "hidden2", "hidden3", "hidden4", "hidden5");
runAs(withUser("user",
READ_TARGET + "/controllerId=in=(" + String.join(", ", List.of(updateTargetControllerIds)) + ")" +
" or controllerId=in=(" + String.join(", ", List.of(readTargetControllerIds)) + ")",
UPDATE_TARGET + "/controllerId=in=(" + String.join(", ", List.of(updateTargetControllerIds)) + ")",
READ_REPOSITORY,
CREATE_ROLLOUT, READ_ROLLOUT), () -> {
final Rollout rollout = testdataFactory.createRolloutByVariables(
"testRollout", "description", updateTargets.size(), "id==*", ds, "50", "5");
assertThat(rollout.getTotalTargets()).isEqualTo(updateTargets.size());
final List<RolloutGroup> content = rolloutGroupManagement.findByRollout(rollout.getId(), Pageable.unpaged()).getContent();
assertThat(content).hasSize(updateTargets.size());
final List<Target> rolloutTargets = content.stream().flatMap(
group -> rolloutGroupManagement.findTargetsOfRolloutGroup(group.getId(), Pageable.unpaged()).get())
.toList();
assertThat(rolloutTargets).hasSize(updateTargets.size()).allMatch(
target -> updateTargets.stream().anyMatch(readTarget -> readTarget.getId().equals(target.getId())))
.noneMatch(target -> readTargets.stream()
.anyMatch(readTarget -> readTarget.getId().equals(target.getId())))
.noneMatch(target -> hiddenTargets.stream()
.anyMatch(readTarget -> readTarget.getId().equals(target.getId())));
});
}
/**
* Verifies only manageable targets are part of an auto assignment.
*/
@Test
void verifyAutoAssignmentTargetScope() {
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
distributionSetManagement.lock(distributionSet.getId());
final String[] updateTargetControllerIds = { "update1", "update2", "update3" };
final List<Target> updateTargets = testdataFactory.createTargets(updateTargetControllerIds);
final String[] readTargetControllerIds = { "read1", "read2", "read3", "read4" };
final List<Target> readTargets = testdataFactory.createTargets(readTargetControllerIds);
final List<Target> hiddenTargets = testdataFactory.createTargets("hidden1", "hidden2", "hidden3", "hidden4", "hidden5");
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
.create(entityFactory.targetFilterQuery().create().name("testName").query("id==*"));
runAs(withUser("user",
READ_TARGET + "/controllerId=in=(" + String.join(", ", List.of(updateTargetControllerIds)) + ")" +
" or controllerId=in=(" + String.join(", ", List.of(readTargetControllerIds)) + ")",
UPDATE_TARGET + "/controllerId=in=(" + String.join(", ", List.of(updateTargetControllerIds)) + ")",
READ_REPOSITORY + "/id==" + distributionSet.getId()), () -> {
targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery()
.updateAutoAssign(targetFilterQuery.getId()).ds(distributionSet.getId()));
autoAssignChecker.checkAllTargets();
assertThat(targetManagement.findByAssignedDistributionSet(distributionSet.getId(), Pageable.unpaged())
.getContent())
.hasSize(updateTargets.size())
.allMatch(assignedTarget -> updateTargets.stream()
.anyMatch(updateTarget -> updateTarget.getId().equals(assignedTarget.getId())))
.noneMatch(assignedTarget -> readTargets.stream()
.anyMatch(updateTarget -> updateTarget.getId().equals(assignedTarget.getId())))
.noneMatch(assignedTarget -> hiddenTargets.stream()
.anyMatch(updateTarget -> updateTarget.getId().equals(assignedTarget.getId())));
});
}
}

View File

@@ -0,0 +1,162 @@
/**
* Copyright (c) 2023 Bosch.IO GmbH and others
*
* 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.repository.jpa.acm;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.eclipse.hawkbit.im.authentication.SpPermission.DELETE_TARGET;
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_TARGET;
import static org.eclipse.hawkbit.im.authentication.SpPermission.UPDATE_TARGET;
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs;
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.withUser;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.builder.TargetTypeCreate;
import org.eclipse.hawkbit.repository.builder.TargetTypeUpdate;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetTypeSpecification;
import org.eclipse.hawkbit.repository.model.TargetType;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Pageable;
import org.springframework.test.context.ContextConfiguration;
/**
* Feature: Component Tests - Access Control<br/>
* Story: Test Target Type Access Controller
*/
@ContextConfiguration(classes = { DefaultAccessControllerConfiguration.class })
class TargetTypeAccessControllerTest extends AbstractJpaIntegrationTest {
/**
* Verifies read access rules for target types
*/
@Test
void verifyTargetTypeReadOperations() {
final TargetType permittedTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type1"));
final TargetType hiddenTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type2"));
runAs(withUser("user", READ_TARGET + "/id==" + permittedTargetType.getId()), () -> {
// verify targetTypeManagement#findAll
assertThat(targetTypeManagement.findAll(Pageable.unpaged()).get().map(Identifiable::getId).toList())
.containsOnly(permittedTargetType.getId());
// verify targetTypeManagement#findByRsql
assertThat(targetTypeManagement.findByRsql("name==*", Pageable.unpaged()).get().map(Identifiable::getId).toList())
.containsOnly(permittedTargetType.getId());
// verify targetTypeManagement#findByName
assertThat(targetTypeManagement.findByName(permittedTargetType.getName(), Pageable.unpaged()).getContent())
.hasSize(1).satisfies(results ->
assertThat(results.get(0).getId()).isEqualTo(permittedTargetType.getId()));
assertThat(targetTypeManagement.findByName(hiddenTargetType.getName(), Pageable.unpaged())).isEmpty();
// verify targetTypeManagement#count
assertThat(targetTypeManagement.count()).isEqualTo(1);
// verify targetTypeManagement#countByName
assertThat(targetTypeManagement.countByName(permittedTargetType.getName())).isEqualTo(1);
assertThat(targetTypeManagement.countByName(hiddenTargetType.getName())).isZero();
// verify targetTypeManagement#countByName
assertThat(targetTypeManagement.countByName(permittedTargetType.getName())).isEqualTo(1);
assertThat(targetTypeManagement.countByName(hiddenTargetType.getName())).isZero();
// verify targetTypeManagement#get by id
assertThat(targetTypeManagement.get(permittedTargetType.getId())).isPresent();
final Long hiddenTargetTypeId = hiddenTargetType.getId();
assertThat(targetTypeManagement.get(hiddenTargetTypeId)).isEmpty();
// verify targetTypeManagement#getByName
assertThat(targetTypeManagement.getByName(permittedTargetType.getName())).isPresent();
assertThat(targetTypeManagement.getByName(hiddenTargetType.getName())).isEmpty();
// verify targetTypeManagement#get by ids
assertThat(targetTypeManagement.get(Arrays.asList(permittedTargetType.getId(), hiddenTargetTypeId))
.stream().map(Identifiable::getId).toList()).containsOnly(permittedTargetType.getId());
// verify targetTypeManagement#update is not possible. Assert exception thrown.
final TargetTypeUpdate targetTypeUpdate = entityFactory.targetType().update(hiddenTargetTypeId)
.name(hiddenTargetType.getName() + "/new").description("newDesc");
assertThatThrownBy(() -> targetTypeManagement.update(targetTypeUpdate))
.as("Target type update shouldn't be allowed since the target type is not visible.")
.isInstanceOf(InsufficientPermissionException.class);
// verify targetTypeManagement#delete is not possible. Assert exception thrown.
assertThatThrownBy(() -> targetTypeManagement.delete(hiddenTargetTypeId))
.as("Target type delete shouldn't be allowed since the target type is not visible.")
.isInstanceOf(InsufficientPermissionException.class);
});
}
/**
* Verifies delete access rules for target types
*/
@Test
void verifyTargetTypeDeleteOperations() {
final TargetType manageableTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type1"));
final TargetType readOnlyTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type2"));
runAs(withUser("user",
READ_TARGET + "/id==" + manageableTargetType.getId() + " or id==" + readOnlyTargetType.getId(),
DELETE_TARGET + "/id==" + manageableTargetType.getId()), () -> {
// delete the manageableTargetType
targetTypeManagement.delete(manageableTargetType.getId());
// verify targetTypeManagement#delete for readOnlyTargetType is not possible
final Long readOnlyTargetTypeId = readOnlyTargetType.getId();
assertThatThrownBy(() -> targetTypeManagement.delete(readOnlyTargetTypeId))
.isInstanceOfAny(InsufficientPermissionException.class, EntityNotFoundException.class);
});
}
/**
* Verifies update operation for target types
*/
@Test
void verifyTargetTypeUpdateOperations() {
final TargetType manageableTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type1"));
final TargetType readOnlyTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type2"));
runAs(withUser("user",
READ_TARGET + "/id==" + manageableTargetType.getId() + " or id==" + readOnlyTargetType.getId(),
UPDATE_TARGET + "/id==" + manageableTargetType.getId()), () -> {
// update the manageableTargetType
targetTypeManagement.update(entityFactory.targetType().update(manageableTargetType.getId())
.name(manageableTargetType.getName() + "/new").description("newDesc"));
// verify targetTypeManagement#update for readOnlyTargetType is not possible
final TargetTypeUpdate targetTypeUpdate = entityFactory.targetType().update(readOnlyTargetType.getId())
.name(readOnlyTargetType.getName() + "/new").description("newDesc");
assertThatThrownBy(() -> targetTypeManagement.update(targetTypeUpdate))
.isInstanceOf(InsufficientPermissionException.class);
});
}
/**
* Verifies create operation blocked by controller
*/
@Test
void verifyTargetTypeCreationBlockedByAccessController() {
runAs(withUser("user", READ_TARGET, UPDATE_TARGET), () -> {
// verify targetTypeManagement#create for any type
final TargetTypeCreate targetTypeCreate = entityFactory.targetType().create().name("type1");
assertThatThrownBy(() -> targetTypeManagement.create(targetTypeCreate))
.as("Target type create shouldn't be allowed since the target type is not visible.")
.isInstanceOf(InsufficientPermissionException.class);
});
}
}

View File

@@ -1,156 +0,0 @@
/**
* Copyright (c) 2023 Bosch.IO GmbH and others
*
* 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.repository.jpa.acm.controller;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.eclipse.hawkbit.ContextAware;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType;
import org.eclipse.hawkbit.security.SecurityContextTenantAware;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.test.context.ContextConfiguration;
@ContextConfiguration(classes = { AbstractAccessControllerTest.AccessControlTestConfig.class })
public abstract class AbstractAccessControllerTest extends AbstractJpaIntegrationTest {
@Autowired
protected TestAccessControlManger testAccessControlManger;
protected static <T> List<T> merge(final List<T> lists0, final List<T> list1) {
final List<T> merge = new ArrayList<>(lists0);
merge.addAll(list1);
return merge;
}
protected void permitAllOperations(final AccessController.Operation operation) {
testAccessControlManger.defineAccessRule(JpaTarget.class, operation, Specification.where(null), type -> true);
testAccessControlManger.defineAccessRule(JpaTargetType.class, operation, Specification.where(null), type -> true);
testAccessControlManger.defineAccessRule(JpaDistributionSet.class, operation, Specification.where(null), type -> true);
}
@BeforeEach
void beforeEach() {
testAccessControlManger.deleteAllRules();
}
@AfterEach
void afterEach() {
testAccessControlManger.deleteAllRules();
}
public static class AccessControlTestConfig {
private final ContextAware contextAware = new SecurityContextTenantAware((tenant, username) -> List.of());
@Bean
public ContextAware contextAware() {
return contextAware;
}
@Bean
public TestAccessControlManger accessControlTestManger() {
return new TestAccessControlManger();
}
@Bean
public AccessController<JpaTarget> targetAccessController(final TestAccessControlManger testAccessControlManger) {
return new AccessController<>() {
@Override
public Optional<Specification<JpaTarget>> getAccessRules(final Operation operation) {
if (contextAware.getCurrentTenant() != null
&& SecurityContextTenantAware.SYSTEM_USER.equals(contextAware.getCurrentUsername())) {
// as tenant, no restrictions
return Optional.empty();
}
return Optional.ofNullable(testAccessControlManger.getAccessRule(JpaTarget.class, operation));
}
@Override
public void assertOperationAllowed(final Operation operation, final JpaTarget entity) throws InsufficientPermissionException {
testAccessControlManger.assertOperation(JpaTarget.class, operation, List.of(entity));
}
@Override
public String toString() {
return AccessController.class.getSimpleName() + '<' + JpaTarget.class.getSimpleName() + '>';
}
};
}
@Bean
public AccessController<JpaTargetType> targetTypeAccessController(final TestAccessControlManger testAccessControlManger) {
return new AccessController<>() {
@Override
public Optional<Specification<JpaTargetType>> getAccessRules(final Operation operation) {
if (contextAware.getCurrentTenant() != null
&& SecurityContextTenantAware.SYSTEM_USER.equals(contextAware.getCurrentUsername())) {
// as tenant, no restrictions
return Optional.empty();
}
return Optional.ofNullable(testAccessControlManger.getAccessRule(JpaTargetType.class, operation));
}
@Override
public void assertOperationAllowed(final Operation operation, final JpaTargetType entity)
throws InsufficientPermissionException {
testAccessControlManger.assertOperation(JpaTargetType.class, operation, List.of(entity));
}
@Override
public String toString() {
return AccessController.class.getSimpleName() + '<' + JpaTargetType.class.getSimpleName() + '>';
}
};
}
@Bean
public AccessController<JpaDistributionSet> distributionSetAccessController(final TestAccessControlManger testAccessControlManger) {
return new AccessController<>() {
@Override
public Optional<Specification<JpaDistributionSet>> getAccessRules(final Operation operation) {
if (contextAware.getCurrentTenant() != null
&& SecurityContextTenantAware.SYSTEM_USER.equals(contextAware.getCurrentUsername())) {
// as tenant, no restrictions
return Optional.empty();
}
return Optional.ofNullable(testAccessControlManger.getAccessRule(JpaDistributionSet.class, operation));
}
@Override
public void assertOperationAllowed(final Operation operation, final JpaDistributionSet entity)
throws InsufficientPermissionException {
testAccessControlManger.assertOperation(JpaDistributionSet.class, operation, List.of(entity));
}
@Override
public String toString() {
return AccessController.class.getSimpleName() + '<' + JpaDistributionSet.class.getSimpleName() + '>';
}
};
}
}
}

View File

@@ -1,327 +0,0 @@
/**
* Copyright (c) 2023 Bosch.IO GmbH and others
*
* 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.repository.jpa.acm.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import jakarta.persistence.criteria.Predicate;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.builder.AutoAssignDistributionSetUpdate;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet_;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetFilter;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
/**
* Feature: Component Tests - Access Control<br/>
* Story: Test Distribution Set Access Controller
*/
class DistributionSetAccessControllerTest extends AbstractAccessControllerTest {
/**
* Verifies read access rules for distribution sets
*/
@Test
void verifyDistributionSetReadOperations() {
permitAllOperations(AccessController.Operation.READ);
permitAllOperations(AccessController.Operation.CREATE);
permitAllOperations(AccessController.Operation.UPDATE);
final DistributionSet permitted = testdataFactory.createDistributionSet();
final DistributionSet hidden = testdataFactory.createDistributionSet();
final Action permittedAction = testdataFactory.performAssignment(permitted);
final Action hiddenAction = testdataFactory.performAssignment(hidden);
testAccessControlManger.deleteAllRules();
// define access controlling rule
defineAccess(AccessController.Operation.READ, permitted);
testAccessControlManger.defineAccessRule(
JpaTarget.class, AccessController.Operation.READ,
TargetSpecifications.hasId(permittedAction.getTarget().getId()),
target -> target.getId().equals(permittedAction.getTarget().getId()));
final Long permittedActionId = permitted.getId();
// verify distributionSetManagement#findAll
assertThat(distributionSetManagement.findAll(Pageable.unpaged()).get().map(Identifiable::getId).toList())
.containsOnly(permittedActionId);
// verify distributionSetManagement#findByRsql
assertThat(distributionSetManagement.findByRsql("name==*", Pageable.unpaged()).get().map(Identifiable::getId)
.toList()).containsOnly(permittedActionId);
// verify distributionSetManagement#findByCompleted
assertThat(distributionSetManagement.findByCompleted(true, Pageable.unpaged()).get().map(Identifiable::getId)
.toList()).containsOnly(permittedActionId);
// verify distributionSetManagement#findByDistributionSetFilter
assertThat(distributionSetManagement
.findByDistributionSetFilter(DistributionSetFilter.builder().isDeleted(false).build(), Pageable.unpaged())
.get().map(Identifiable::getId).toList()).containsOnly(permittedActionId);
// verify distributionSetManagement#get
assertThat(distributionSetManagement.get(permittedActionId)).isPresent();
final Long hiddenId = hidden.getId();
assertThat(distributionSetManagement.get(hiddenId)).isEmpty();
// verify distributionSetManagement#getWithDetails
assertThat(distributionSetManagement.getWithDetails(permittedActionId)).isPresent();
assertThat(distributionSetManagement.getWithDetails(hiddenId)).isEmpty();
// verify distributionSetManagement#get
assertThat(distributionSetManagement.getValid(permittedActionId).getId()).isEqualTo(permittedActionId);
assertThatThrownBy(() -> distributionSetManagement.getValid(hiddenId))
.as("Distribution set should not be found.").isInstanceOf(EntityNotFoundException.class);
// verify distributionSetManagement#get
final List<Long> allActionIds = Arrays.asList(permittedActionId, hiddenId);
assertThatThrownBy(() -> distributionSetManagement.get(allActionIds))
.as("Fail if request hidden.").isInstanceOf(EntityNotFoundException.class);
// verify distributionSetManagement#getByNameAndVersion
assertThat(distributionSetManagement.findByNameAndVersion(permitted.getName(), permitted.getVersion())).isPresent();
assertThat(distributionSetManagement.findByNameAndVersion(hidden.getName(), hidden.getVersion())).isEmpty();
// verify distributionSetManagement#getByAction
assertThat(distributionSetManagement.findByAction(permittedAction.getId())).isPresent();
final Long hiddenActionId = hiddenAction.getId();
assertThatThrownBy(() -> distributionSetManagement.findByAction(hiddenActionId))
.as("Action is hidden.").isInstanceOf(InsufficientPermissionException.class);
}
/**
* Verifies read access rules for distribution sets
*/
@Test
void verifyDistributionSetUpdates() {
// permit all operations first to prepare test setup
permitAllOperations(AccessController.Operation.READ);
permitAllOperations(AccessController.Operation.CREATE);
permitAllOperations(AccessController.Operation.UPDATE);
final DistributionSet permitted = testdataFactory.createDistributionSet();
final String mdPresetKey = "metadata.preset";
final String mdPresetValue = "presetValue";
distributionSetManagement.createMetadata(permitted.getId(), Map.of(mdPresetKey, mdPresetValue));
final DistributionSet readOnly = testdataFactory.createDistributionSet();
distributionSetManagement.createMetadata(readOnly.getId(), Map.of(mdPresetKey, mdPresetValue));
final DistributionSet hidden = testdataFactory.createDistributionSet();
distributionSetManagement.createMetadata(hidden.getId(), Map.of(mdPresetKey, mdPresetValue));
final SoftwareModule swModule = testdataFactory.createSoftwareModuleOs();
// entities created - reset rules
testAccessControlManger.deleteAllRules();
// define access controlling rule
defineAccess(AccessController.Operation.READ, permitted, readOnly);
defineAccess(AccessController.Operation.UPDATE, permitted);
// verify distributionSetManagement#assignSoftwareModules
final List<Long> singleModuleIdList = Collections.singletonList(swModule.getId());
assertThat(distributionSetManagement.assignSoftwareModules(permitted.getId(), singleModuleIdList))
.satisfies(ds -> assertThat(ds.getModules().stream().map(Identifiable::getId).toList()).contains(swModule.getId()));
final Long readOnlyId = readOnly.getId();
assertThatThrownBy(() -> distributionSetManagement.assignSoftwareModules(readOnlyId, singleModuleIdList))
.as("Distribution set not allowed to me modified.")
.isInstanceOf(InsufficientPermissionException.class);
final Long hiddenId = hidden.getId();
assertThatThrownBy(() -> distributionSetManagement.assignSoftwareModules(hiddenId, singleModuleIdList))
.as("Distribution set should not be visible.")
.isInstanceOf(EntityNotFoundException.class);
final Map<String, String> metadata = Map.of("test.create", mdPresetValue);
// verify distributionSetManagement#createMetaData
distributionSetManagement.createMetadata(permitted.getId(), metadata);
assertThatThrownBy(() -> distributionSetManagement.createMetadata(readOnlyId, metadata))
.as("Distribution set not allowed to be modified.")
.isInstanceOf(InsufficientPermissionException.class);
assertThatThrownBy(() -> distributionSetManagement.createMetadata(hiddenId, metadata))
.as("Distribution set should not be visible.")
.isInstanceOf(EntityNotFoundException.class);
// verify distributionSetManagement#updateMetaData
final String newValue = "newValue";
distributionSetManagement.updateMetadata(permitted.getId(), mdPresetKey, newValue);
assertThatThrownBy(() -> distributionSetManagement.updateMetadata(readOnlyId, mdPresetKey, newValue))
.as("Distribution set not allowed to me modified.")
.isInstanceOf(InsufficientPermissionException.class);
assertThatThrownBy(() -> distributionSetManagement.updateMetadata(hiddenId, mdPresetKey, newValue))
.as("Distribution set should not be visible.")
.isInstanceOf(EntityNotFoundException.class);
// verify distributionSetManagement#deleteMetaData
final String metadataKey = metadata.entrySet().stream().findAny().get().getKey();
distributionSetManagement.deleteMetadata(permitted.getId(), metadataKey);
assertThatThrownBy(() -> distributionSetManagement.deleteMetadata(readOnlyId, mdPresetKey))
.as("Distribution set not allowed to me modified.")
.isInstanceOf(InsufficientPermissionException.class);
assertThatThrownBy(() -> distributionSetManagement.deleteMetadata(hiddenId, mdPresetKey))
.as("Distribution set should not be visible.")
.isInstanceOf(EntityNotFoundException.class);
}
@Test
void verifyTagFilteringAndManagement() {
// permit all operations first to prepare test setup
permitAllOperations(AccessController.Operation.READ);
permitAllOperations(AccessController.Operation.CREATE);
permitAllOperations(AccessController.Operation.UPDATE);
final DistributionSet permitted = testdataFactory.createDistributionSet();
final DistributionSet readOnly = testdataFactory.createDistributionSet();
final DistributionSet hidden = testdataFactory.createDistributionSet();
final Long dsTagId = distributionSetTagManagement.create(entityFactory.tag().create().name("dsTag")).getId();
final Long dsTag2Id = distributionSetTagManagement.create(entityFactory.tag().create().name("dsTag2")).getId();
// perform tag assignment before setting access rules
distributionSetManagement.assignTag(Arrays.asList(permitted.getId(), readOnly.getId(), hidden.getId()),
dsTagId);
// entities created - reset rules
testAccessControlManger.deleteAllRules();
// define access controlling rule
defineAccess(AccessController.Operation.READ, permitted, readOnly);
// allow updating the permitted distributionSet
defineAccess(AccessController.Operation.UPDATE, permitted);
assertThat(distributionSetManagement.findByTag(dsTagId, Pageable.unpaged()).get().map(Identifiable::getId)
.toList()).containsOnly(permitted.getId(), readOnly.getId());
assertThat(distributionSetManagement.findByRsqlAndTag("name==*", dsTagId, Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permitted.getId(), readOnly.getId());
// verify distributionSetManagement#unassignTag on permitted target
assertThat(distributionSetManagement
.unassignTag(Collections.singletonList(permitted.getId()), dsTagId))
.size()
.isEqualTo(1);
// verify distributionSetManagement#assignTag on permitted target
assertThat(distributionSetManagement.assignTag(Collections.singletonList(permitted.getId()), dsTagId))
.hasSize(1);
// verify distributionSetManagement#unAssignTag on permitted target
assertThat(distributionSetManagement.unassignTag(List.of(permitted.getId()), dsTagId)
.get(0).getId())
.isEqualTo(permitted.getId());
// assignment is denied for readOnlyTarget (read, but no update permissions)
final List<Long> readOblyList = Collections.singletonList(readOnly.getId());
assertThatThrownBy(() ->
distributionSetManagement.unassignTag(readOblyList, dsTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(InsufficientPermissionException.class);
// assignment is denied for readOnlyTarget (read, but no update permissions)
// dsTag2- since - it is tagged with dsTag and won't do anything if assigning dsTag
assertThatThrownBy(() -> {
distributionSetManagement.assignTag(readOblyList, dsTag2Id);
}).as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(InsufficientPermissionException.class);
// assignment is denied for hiddenTarget since it's hidden
final List<Long> hiddenList = Collections.singletonList(hidden.getId());
assertThatThrownBy(() -> distributionSetManagement.unassignTag(hiddenList, dsTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(EntityNotFoundException.class);
// assignment is denied for hiddenTarget since it's hidden
assertThatThrownBy(() -> {
distributionSetManagement.assignTag(hiddenList, dsTagId);
}).as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(EntityNotFoundException.class);
// assignment is denied for hiddenTarget since it's hidden
final List<Long> hiddenIdList = List.of(hidden.getId());
assertThatThrownBy(() -> distributionSetManagement.unassignTag(hiddenIdList, dsTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(EntityNotFoundException.class);
}
@Test
void verifyAutoAssignmentUsage() {
// permit all operations first to prepare test setup
permitAllOperations(AccessController.Operation.READ);
permitAllOperations(AccessController.Operation.CREATE);
permitAllOperations(AccessController.Operation.UPDATE);
final DistributionSet permitted = testdataFactory.createDistributionSet();
final DistributionSet readOnly = testdataFactory.createDistributionSet();
final DistributionSet hidden = testdataFactory.createDistributionSet();
// has to lock them, otherwise implicit lock shall be made which require DistributionSet update permissions
distributionSetManagement.lock(permitted.getId());
distributionSetManagement.lock(readOnly.getId());
distributionSetManagement.lock(hidden.getId());
// entities created - reset rules
testAccessControlManger.deleteAllRules();
// define read access
defineAccess(AccessController.Operation.READ, permitted, readOnly);
// permit update operation
defineAccess(AccessController.Operation.UPDATE, permitted);
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
.create(entityFactory.targetFilterQuery().create().name("test").query("id==*"));
assertThat(targetFilterQueryManagement
.updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId()).ds(permitted.getId())
.actionType(Action.ActionType.FORCED).confirmationRequired(false))
.getAutoAssignDistributionSet().getId()).isEqualTo(permitted.getId());
targetFilterQueryManagement
.updateAutoAssignDS(new AutoAssignDistributionSetUpdate(targetFilterQuery.getId())
.ds(readOnly.getId()).actionType(Action.ActionType.FORCED).confirmationRequired(false))
.getAutoAssignDistributionSet().getId();
final AutoAssignDistributionSetUpdate autoAssignDistributionSetUpdate = new AutoAssignDistributionSetUpdate(targetFilterQuery.getId())
.ds(hidden.getId()).actionType(Action.ActionType.FORCED).confirmationRequired(false);
assertThatThrownBy(() -> targetFilterQueryManagement.updateAutoAssignDS(autoAssignDistributionSetUpdate))
.isInstanceOf(EntityNotFoundException.class);
}
private void defineAccess(final AccessController.Operation operation, final DistributionSet... distributionSets) {
defineAccess(operation, List.of(distributionSets));
}
private void defineAccess(final AccessController.Operation operation, final List<DistributionSet> targets) {
final List<Long> ids = targets.stream().map(DistributionSet::getId).toList();
testAccessControlManger.defineAccessRule(
JpaDistributionSet.class, operation,
dsByIds(ids),
distributionSet -> ids.contains(distributionSet.getId()));
}
private static Specification<JpaDistributionSet> dsByIds(final Collection<Long> distids) {
return (dsRoot, query, cb) -> {
final Predicate predicate = dsRoot.get(JpaDistributionSet_.id).in(distids);
query.distinct(true);
return predicate;
};
}
}

View File

@@ -1,418 +0,0 @@
/**
* Copyright (c) 2023 Bosch.IO GmbH and others
*
* 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.repository.jpa.acm.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import jakarta.persistence.criteria.Predicate;
import org.eclipse.hawkbit.repository.FilterParams;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
import org.eclipse.hawkbit.repository.jpa.autoassign.AutoAssignChecker;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet_;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
import org.eclipse.hawkbit.repository.model.Action;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.RolloutGroup;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
/**
* Feature: Component Tests - Access Control<br/>
* Story: Test Target Access Controller
*/
class TargetAccessControllerTest extends AbstractAccessControllerTest {
@Autowired
AutoAssignChecker autoAssignChecker;
/**
* Verifies read access rules for targets
*/
@Test
void verifyTargetReadOperations() {
permitAllOperations(AccessController.Operation.CREATE);
final Target permittedTarget = targetManagement
.create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED));
final Target hiddenTarget = targetManagement
.create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED));
// define access controlling rule
defineAccess(AccessController.Operation.READ, permittedTarget);
// verify targetManagement#findAll
assertThat(targetManagement.findAll(Pageable.unpaged()).get().map(Identifiable::getId).toList())
.containsOnly(permittedTarget.getId());
// verify targetManagement#findByRsql
assertThat(targetManagement.findByRsql("id==*", Pageable.unpaged()).get().map(Identifiable::getId).toList())
.containsOnly(permittedTarget.getId());
// verify targetManagement#findByUpdateStatus
assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.REGISTERED, Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId());
// verify targetManagement#getByControllerID
assertThat(targetManagement.getByControllerID(permittedTarget.getControllerId())).isPresent();
final String hiddenTargetControllerId = hiddenTarget.getControllerId();
assertThatThrownBy(() -> targetManagement.getByControllerID(hiddenTargetControllerId))
.as("Missing read permissions for hidden target.")
.isInstanceOf(InsufficientPermissionException.class);
// verify targetManagement#getByControllerID
assertThat(targetManagement
.getByControllerID(Arrays.asList(permittedTarget.getControllerId(), hiddenTargetControllerId))
.stream().map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId());
// verify targetManagement#get
assertThat(targetManagement.get(permittedTarget.getId())).isPresent();
assertThat(targetManagement.get(hiddenTarget.getId())).isEmpty();
// verify targetManagement#get
assertThat(targetManagement.get(Arrays.asList(permittedTarget.getId(), hiddenTarget.getId())).stream()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId());
// verify targetManagement#getControllerAttributes
assertThat(targetManagement.getControllerAttributes(permittedTarget.getControllerId())).isEmpty();
assertThatThrownBy(() -> targetManagement.getControllerAttributes(hiddenTargetControllerId))
.as("Target should not be found.")
.isInstanceOf(InsufficientPermissionException.class);
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
.create(entityFactory.targetFilterQuery().create().name("test").query("id==*"));
// verify targetManagement#findByTargetFilterQuery
assertThat(targetManagement.findByTargetFilterQuery(targetFilterQuery.getId(), Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId());
// verify targetManagement#findByTargetFilterQuery (used by UI)
assertThat(targetManagement.findByFilters(new FilterParams(null, null, null, null), Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId());
}
@Test
void verifyTagFilteringAndManagement() {
// permit all operations first to prepare test setup
permitAllOperations(AccessController.Operation.READ);
permitAllOperations(AccessController.Operation.CREATE);
permitAllOperations(AccessController.Operation.UPDATE);
final Target permittedTarget = targetManagement
.create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED));
final Target readOnlyTarget = targetManagement
.create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED));
final String readOnlyTargetControllerId = readOnlyTarget.getControllerId();
final Target hiddenTarget = targetManagement
.create(entityFactory.target().create().controllerId("device03").status(TargetUpdateStatus.REGISTERED));
final Long myTagId = targetTagManagement.create(entityFactory.tag().create().name("myTag")).getId();
// perform tag assignment before setting access rules
targetManagement.assignTag(Arrays.asList(permittedTarget.getControllerId(), readOnlyTargetControllerId,
hiddenTarget.getControllerId()), myTagId);
// define access controlling rule
testAccessControlManger.deleteAllRules();
defineAccess(AccessController.Operation.READ, permittedTarget, readOnlyTarget);
// allow update operation
// allow update operation
defineAccess(AccessController.Operation.UPDATE, permittedTarget);
// verify targetManagement#findByTag
assertThat(
targetManagement.findByTag(myTagId, Pageable.unpaged()).get().map(Identifiable::getId).toList())
.containsOnly(permittedTarget.getId(), readOnlyTarget.getId());
// verify targetManagement#findByRsqlAndTag
assertThat(targetManagement.findByRsqlAndTag("id==*", myTagId, Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId(), readOnlyTarget.getId());
// verify targetManagement#assignTag on permitted target
assertThat(targetManagement.assignTag(Collections.singletonList(permittedTarget.getControllerId()), myTagId))
.hasSize(1);
// verify targetManagement#unassignTag on permitted target
assertThat(targetManagement.unassignTag(Collections.singletonList(permittedTarget.getControllerId()), myTagId))
.hasSize(1);
// verify targetManagement#assignTag on permitted target
assertThat(targetManagement.assignTag(Collections.singletonList(permittedTarget.getControllerId()), myTagId))
.hasSize(1);
// verify targetManagement#unAssignTag on permitted target
assertThat(targetManagement.unassignTag(List.of(permittedTarget.getControllerId()), myTagId).get(0).getControllerId())
.isEqualTo(permittedTarget.getControllerId());
// assignment is denied for readOnlyTarget (read, but no update permissions)
// No exception has been thrown - because no real change is done
// assertThatThrownBy(() -> {
// targetManagement
// .assignTag(List.of(readOnlyTarget.getControllerId()), myTag.getId())
// .getUnassigned();
// }).as("Missing update permissions for target to toggle tag assignment.")
// .isInstanceOf(InsufficientPermissionException.class);
// assignment is denied for readOnlyTarget (read, but no update permissions)
final List<String> readTargetControllerIdList = Collections.singletonList(readOnlyTargetControllerId);
assertThatThrownBy(() -> targetManagement.assignTag(readTargetControllerIdList, myTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOfAny(InsufficientPermissionException.class);
// assignment is denied for readOnlyTarget (read, but no update permissions)
final List<String> readOnlyTargetControllerIdList = List.of(readOnlyTargetControllerId);
assertThatThrownBy(() -> targetManagement.unassignTag(readOnlyTargetControllerIdList, myTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(InsufficientPermissionException.class);
// assignment is denied for hiddenTarget since it's hidden
final List<String> hiddenTargetControllerIdList = Collections.singletonList(hiddenTarget.getControllerId());
assertThatThrownBy(() -> targetManagement.assignTag(hiddenTargetControllerIdList, myTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(InsufficientPermissionException.class);
// assignment is denied for hiddenTarget since it's hidden
assertThatThrownBy(() -> targetManagement.assignTag(hiddenTargetControllerIdList, myTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(InsufficientPermissionException.class);
// assignment is denied for hiddenTarget since it's hidden
assertThatThrownBy(() -> targetManagement.unassignTag(hiddenTargetControllerIdList, myTagId))
.as("Missing update permissions for target to toggle tag assignment.")
.isInstanceOf(InsufficientPermissionException.class);
}
/**
* Verifies rules for target assignment
*/
@Test
void verifyTargetAssignment() {
permitAllOperations(AccessController.Operation.READ);
permitAllOperations(AccessController.Operation.CREATE);
permitAllOperations(AccessController.Operation.UPDATE);
final Long dsId = testdataFactory.createDistributionSet("myDs").getId();
distributionSetManagement.lock(dsId);
// entities created - reset rules
testAccessControlManger.deleteAllRules();
permitAllOperations(AccessController.Operation.READ);
permitAllOperations(AccessController.Operation.CREATE);
final Target permittedTarget = targetManagement
.create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED));
final String hiddenTargetControllerId = targetManagement
.create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED))
.getControllerId();
// define access controlling rule
overwriteAccess(AccessController.Operation.READ, permittedTarget);
// verify targetManagement#findByUpdateStatus before assignment
assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.REGISTERED, Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId());
testAccessControlManger.defineAccessRule(
JpaTarget.class, AccessController.Operation.UPDATE,
TargetSpecifications.hasId(permittedTarget.getId()),
target -> target.getId().equals(permittedTarget.getId()));
assertThat(assignDistributionSet(dsId, permittedTarget.getControllerId()).getAssigned()).isEqualTo(1);
// assigning of non allowed target behaves as not found
assertThatThrownBy(() -> assignDistributionSet(dsId, hiddenTargetControllerId)).isInstanceOf(AssertionError.class);
// verify targetManagement#findByUpdateStatus(REGISTERED) after assignment
assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.REGISTERED, Pageable.unpaged())
.getTotalElements()).isZero();
// verify targetManagement#findByUpdateStatus(PENDING) after assignment
assertThat(targetManagement.findByUpdateStatus(TargetUpdateStatus.PENDING, Pageable.unpaged()).get()
.map(Identifiable::getId).toList()).containsOnly(permittedTarget.getId());
}
/**
* Verifies rules for target assignment
*/
@Test
void verifyTargetAssignmentOnNonUpdatableTarget() {
permitAllOperations(AccessController.Operation.READ);
permitAllOperations(AccessController.Operation.CREATE);
permitAllOperations(AccessController.Operation.UPDATE);
final Long firstDsId = testdataFactory.createDistributionSet("myDs").getId();
distributionSetManagement.lock(firstDsId);
final DistributionSet secondDs = testdataFactory.createDistributionSet("anotherDs");
distributionSetManagement.lock(secondDs.getId());
// entities created - reset rules
testAccessControlManger.deleteAllRules();
permitAllOperations(AccessController.Operation.READ);
permitAllOperations(AccessController.Operation.CREATE);
final Target manageableTarget = targetManagement
.create(entityFactory.target().create().controllerId("device01").status(TargetUpdateStatus.REGISTERED));
final Target readOnlyTarget = targetManagement
.create(entityFactory.target().create().controllerId("device02").status(TargetUpdateStatus.REGISTERED));
// overwriting full access controlling rule
overwriteAccess(AccessController.Operation.READ, manageableTarget, readOnlyTarget);
overwriteAccess(AccessController.Operation.UPDATE, manageableTarget);
// assignment is permitted for manageableTarget
assertThat(assignDistributionSet(firstDsId, manageableTarget.getControllerId()).getAssigned()).isEqualTo(1);
// assignment is denied for readOnlyTarget (read, but no update permissions)
final var readOnlyTargetControllerId = readOnlyTarget.getControllerId();
assertThatThrownBy(() -> assignDistributionSet(firstDsId, readOnlyTargetControllerId)).isInstanceOf(AssertionError.class);
// bunch assignment skips denied denied since at least one target without update
// permissions is present
assertThat(assignDistributionSet(secondDs.getId(),
Arrays.asList(readOnlyTargetControllerId, manageableTarget.getControllerId()),
Action.ActionType.FORCED).getAssigned()).isEqualTo(1);
}
/**
* Verifies only manageable targets are part of the rollout
*/
@Test
void verifyRolloutTargetScope() {
permitAllOperations(AccessController.Operation.READ);
permitAllOperations(AccessController.Operation.CREATE);
permitAllOperations(AccessController.Operation.UPDATE);
final DistributionSet ds = testdataFactory.createDistributionSet("myDs");
distributionSetManagement.lock(ds.getId());
// entities created - reset rules
testAccessControlManger.deleteAllRules();
permitAllOperations(AccessController.Operation.READ);
permitAllOperations(AccessController.Operation.CREATE);
final List<Target> updateTargets = testdataFactory.createTargets("update1", "update2", "update3");
final List<Target> readTargets = testdataFactory.createTargets("read1", "read2", "read3", "read4");
final List<Target> hiddenTargets = testdataFactory.createTargets("hidden1", "hidden2", "hidden3", "hidden4", "hidden5");
defineAccess(AccessController.Operation.UPDATE, updateTargets);
overwriteAccess(AccessController.Operation.READ, merge(readTargets, updateTargets));
final Rollout rollout = testdataFactory.createRolloutByVariables(
"testRollout", "description", updateTargets.size(), "id==*", ds, "50", "5");
assertThat(rollout.getTotalTargets()).isEqualTo(updateTargets.size());
final List<RolloutGroup> content = rolloutGroupManagement.findByRollout(rollout.getId(), Pageable.unpaged()).getContent();
assertThat(content).hasSize(updateTargets.size());
final List<Target> rolloutTargets = content.stream().flatMap(
group -> rolloutGroupManagement.findTargetsOfRolloutGroup(group.getId(), Pageable.unpaged()).get())
.toList();
assertThat(rolloutTargets).hasSize(updateTargets.size()).allMatch(
target -> updateTargets.stream().anyMatch(readTarget -> readTarget.getId().equals(target.getId())))
.noneMatch(target -> readTargets.stream()
.anyMatch(readTarget -> readTarget.getId().equals(target.getId())))
.noneMatch(target -> hiddenTargets.stream()
.anyMatch(readTarget -> readTarget.getId().equals(target.getId())));
}
/**
* Verifies only manageable targets are part of an auto assignment.
*/
@Test
void verifyAutoAssignmentTargetScope() {
permitAllOperations(AccessController.Operation.READ);
permitAllOperations(AccessController.Operation.CREATE);
permitAllOperations(AccessController.Operation.UPDATE);
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
distributionSetManagement.lock(distributionSet.getId());
// entities created - reset rules
testAccessControlManger.deleteAllRules();
permitAllOperations(AccessController.Operation.CREATE);
final List<Target> updateTargets = testdataFactory.createTargets("update1", "update2", "update3");
final List<Target> readTargets = testdataFactory.createTargets("read1", "read2", "read3", "read4");
final List<Target> hiddenTargets = testdataFactory.createTargets("hidden1", "hidden2", "hidden3", "hidden4",
"hidden5");
defineAccess(AccessController.Operation.UPDATE, updateTargets);
defineAccess(AccessController.Operation.READ, merge(updateTargets, readTargets));
final TargetFilterQuery targetFilterQuery = targetFilterQueryManagement
.create(entityFactory.targetFilterQuery().create().name("testName").query("id==*"));
testAccessControlManger.defineAccessRule(
JpaDistributionSet.class, AccessController.Operation.READ,
dsById(distributionSet.getId()),
ds -> ds.getId().equals(distributionSet.getId()));
targetFilterQueryManagement.updateAutoAssignDS(entityFactory.targetFilterQuery()
.updateAutoAssign(targetFilterQuery.getId()).ds(distributionSet.getId()));
autoAssignChecker.checkAllTargets();
assertThat(targetManagement.findByAssignedDistributionSet(distributionSet.getId(), Pageable.unpaged())
.getContent())
.hasSize(updateTargets.size())
.allMatch(assignedTarget -> updateTargets.stream()
.anyMatch(updateTarget -> updateTarget.getId().equals(assignedTarget.getId())))
.noneMatch(assignedTarget -> readTargets.stream()
.anyMatch(updateTarget -> updateTarget.getId().equals(assignedTarget.getId())))
.noneMatch(assignedTarget -> hiddenTargets.stream()
.anyMatch(updateTarget -> updateTarget.getId().equals(assignedTarget.getId())));
}
private void defineAccess(final AccessController.Operation operation, final Target... target) {
defineAccess(operation, List.of(target));
}
private void defineAccess(final AccessController.Operation operation, final List<Target> targets) {
final List<Long> ids = targets.stream().map(Target::getId).toList();
testAccessControlManger.defineAccessRule(
JpaTarget.class, operation,
TargetSpecifications.hasIdIn(ids),
target -> ids.contains(target.getId()));
}
private void overwriteAccess(final AccessController.Operation operation, final Target... target) {
overwriteAccess(operation, List.of(target));
}
private void overwriteAccess(final AccessController.Operation operation, final List<Target> targets) {
final List<Long> ids = targets.stream().map(Target::getId).toList();
testAccessControlManger.overwriteAccessRule(
JpaTarget.class, operation,
TargetSpecifications.hasIdIn(ids),
target -> ids.contains(target.getId()));
}
private static Specification<JpaDistributionSet> dsById(final Long distid) {
return (dsRoot, query, cb) -> {
final Predicate predicate = cb.equal(dsRoot.get(JpaDistributionSet_.id), distid);
query.distinct(true);
return predicate;
};
}
}

View File

@@ -1,174 +0,0 @@
/**
* Copyright (c) 2023 Bosch.IO GmbH and others
*
* 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.repository.jpa.acm.controller;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import org.eclipse.hawkbit.repository.Identifiable;
import org.eclipse.hawkbit.repository.builder.TargetTypeCreate;
import org.eclipse.hawkbit.repository.builder.TargetTypeUpdate;
import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetTypeSpecification;
import org.eclipse.hawkbit.repository.model.TargetType;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Pageable;
/**
* Feature: Component Tests - Access Control<br/>
* Story: Test Target Type Access Controller
*/
class TargetTypeAccessControllerTest extends AbstractAccessControllerTest {
/**
* Verifies read access rules for target types
*/
@Test
void verifyTargetTypeReadOperations() {
permitAllOperations(AccessController.Operation.CREATE);
final TargetType permittedTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type1"));
final TargetType hiddenTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type2"));
// define access controlling rule
defineAccess(AccessController.Operation.READ, permittedTargetType);
// verify targetTypeManagement#findAll
assertThat(targetTypeManagement.findAll(Pageable.unpaged()).get().map(Identifiable::getId).toList())
.containsOnly(permittedTargetType.getId());
// verify targetTypeManagement#findByRsql
assertThat(targetTypeManagement.findByRsql("name==*", Pageable.unpaged()).get().map(Identifiable::getId).toList())
.containsOnly(permittedTargetType.getId());
// verify targetTypeManagement#findByName
assertThat(targetTypeManagement.findByName(permittedTargetType.getName(), Pageable.unpaged()).getContent())
.hasSize(1).satisfies(results ->
assertThat(results.get(0).getId()).isEqualTo(permittedTargetType.getId()));
assertThat(targetTypeManagement.findByName(hiddenTargetType.getName(), Pageable.unpaged())).isEmpty();
// verify targetTypeManagement#count
assertThat(targetTypeManagement.count()).isEqualTo(1);
// verify targetTypeManagement#countByName
assertThat(targetTypeManagement.countByName(permittedTargetType.getName())).isEqualTo(1);
assertThat(targetTypeManagement.countByName(hiddenTargetType.getName())).isZero();
// verify targetTypeManagement#countByName
assertThat(targetTypeManagement.countByName(permittedTargetType.getName())).isEqualTo(1);
assertThat(targetTypeManagement.countByName(hiddenTargetType.getName())).isZero();
// verify targetTypeManagement#get by id
assertThat(targetTypeManagement.get(permittedTargetType.getId())).isPresent();
final Long hiddenTargetTypeId = hiddenTargetType.getId();
assertThat(targetTypeManagement.get(hiddenTargetTypeId)).isEmpty();
// verify targetTypeManagement#getByName
assertThat(targetTypeManagement.getByName(permittedTargetType.getName())).isPresent();
assertThat(targetTypeManagement.getByName(hiddenTargetType.getName())).isEmpty();
// verify targetTypeManagement#get by ids
assertThat(targetTypeManagement.get(Arrays.asList(permittedTargetType.getId(), hiddenTargetTypeId))
.stream().map(Identifiable::getId).toList()).containsOnly(permittedTargetType.getId());
// verify targetTypeManagement#update is not possible. Assert exception thrown.
final TargetTypeUpdate targetTypeUpdate = entityFactory.targetType().update(hiddenTargetTypeId)
.name(hiddenTargetType.getName() + "/new").description("newDesc");
assertThatThrownBy(() -> targetTypeManagement.update(targetTypeUpdate))
.as("Target type update shouldn't be allowed since the target type is not visible.")
.isInstanceOf(EntityNotFoundException.class);
// verify targetTypeManagement#delete is not possible. Assert exception thrown.
assertThatThrownBy(() -> targetTypeManagement.delete(hiddenTargetTypeId))
.as("Target type delete shouldn't be allowed since the target type is not visible.")
.isInstanceOf(EntityNotFoundException.class);
}
/**
* Verifies delete access rules for target types
*/
@Test
void verifyTargetTypeDeleteOperations() {
permitAllOperations(AccessController.Operation.CREATE);
final TargetType manageableTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type1"));
final TargetType readOnlyTargetType = targetTypeManagement.create(entityFactory.targetType().create().name("type2"));
// define access controlling rule to allow reading both types
defineAccess(AccessController.Operation.READ, manageableTargetType, readOnlyTargetType);
// permit operation to delete permittedTargetType
defineAccess(AccessController.Operation.DELETE, manageableTargetType);
// delete the manageableTargetType
targetTypeManagement.delete(manageableTargetType.getId());
// verify targetTypeManagement#delete for readOnlyTargetType is not possible
final Long readOnlyTargetTypeId = readOnlyTargetType.getId();
assertThatThrownBy(() -> targetTypeManagement.delete(readOnlyTargetTypeId))
.isInstanceOfAny(InsufficientPermissionException.class, EntityNotFoundException.class);
}
/**
* Verifies update operation for target types
*/
@Test
void verifyTargetTypeUpdateOperations() {
permitAllOperations(AccessController.Operation.CREATE);
final TargetType manageableTargetType = targetTypeManagement
.create(entityFactory.targetType().create().name("type1"));
final TargetType readOnlyTargetType = targetTypeManagement
.create(entityFactory.targetType().create().name("type2"));
// define access controlling rule to allow reading both types
defineAccess(AccessController.Operation.READ, manageableTargetType, readOnlyTargetType);
// permit updating the manageableTargetType
defineAccess(AccessController.Operation.UPDATE, manageableTargetType);
// update the manageableTargetType
targetTypeManagement.update(entityFactory.targetType().update(manageableTargetType.getId())
.name(manageableTargetType.getName() + "/new").description("newDesc"));
// verify targetTypeManagement#update for readOnlyTargetType is not possible
final TargetTypeUpdate targetTypeUpdate = entityFactory.targetType().update(readOnlyTargetType.getId())
.name(readOnlyTargetType.getName() + "/new").description("newDesc");
assertThatThrownBy(() -> targetTypeManagement.update(targetTypeUpdate))
.isInstanceOf(InsufficientPermissionException.class);
}
/**
* Verifies create operation blocked by controller
*/
@Test
void verifyTargetTypeCreationBlockedByAccessController() {
defineAccess(AccessController.Operation.CREATE); // allows for none
// verify targetTypeManagement#create for any type
final TargetTypeCreate targetTypeCreate = entityFactory.targetType().create().name("type1");
assertThatThrownBy(() -> targetTypeManagement.create(targetTypeCreate))
.as("Target type create shouldn't be allowed since the target type is not visible.")
.isInstanceOf(InsufficientPermissionException.class);
}
private void defineAccess(final AccessController.Operation operation, final TargetType... targetTypes) {
final List<Long> ids = Stream.of(targetTypes).map(TargetType::getId).toList();
testAccessControlManger.defineAccessRule(
JpaTargetType.class, operation,
TargetTypeSpecification.hasIdIn(ids),
targetType -> ids.contains(targetType.getId()));
}
}

View File

@@ -1,85 +0,0 @@
/**
* Copyright (c) 2023 Bosch.IO GmbH and others
*
* 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.repository.jpa.acm.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaBaseEntity;
import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaBaseEntity_;
import org.springframework.data.jpa.domain.Specification;
public class TestAccessControlManger {
private final Map<AccessRuleId<?>, AccessRule<?>> accessRules = new HashMap<>();
public void deleteAllRules() {
accessRules.clear();
}
public <T> void defineAccessRule(
final Class<T> ruleClass, final AccessController.Operation operation,
final Specification<T> specification, final Predicate<T> check) {
defineAccessRule(ruleClass, operation, specification, check, false);
}
public <T> void overwriteAccessRule(
final Class<T> ruleClass, final AccessController.Operation operation,
final Specification<T> specification, final Predicate<T> check) {
defineAccessRule(ruleClass, operation, specification, check, true);
}
private <T> void defineAccessRule(
final Class<T> ruleClass, final AccessController.Operation operation,
final Specification<T> specification, final Predicate<T> check, final boolean overwrite) {
final AccessRuleId<T> ruleId = new AccessRuleId<>(ruleClass, operation);
if (!overwrite && accessRules.containsKey(ruleId)) {
throw new IllegalStateException("Access rule already defined for " + ruleId + "! You should explicitly set overwrite to true.");
}
accessRules.put(ruleId, new AccessRule<>(specification, check));
}
public <T extends AbstractJpaBaseEntity> Specification<T> getAccessRule(final Class<T> ruleClass,
final AccessController.Operation operation) {
@SuppressWarnings("unchecked")
final AccessRule<T> accessRule = (AccessRule<T>) accessRules.getOrDefault(new AccessRuleId<>(ruleClass, operation), null);
if (accessRule == null) {
return nop();
} else {
return accessRule.specification();
}
}
public <T> void assertOperation(final Class<T> ruleClass, final AccessController.Operation operation, final List<T> entities) {
@SuppressWarnings("unchecked")
final AccessRule<T> accessRule = (AccessRule<T>) accessRules.getOrDefault(new AccessRuleId<>(ruleClass, operation), null);
if (accessRule == null) {
throw new InsufficientPermissionException("No access define - reject all");
} else {
for (final T entity : entities) {
if (!accessRule.checker.test(entity)) {
throw new InsufficientPermissionException("Access to " + ruleClass.getName() + "/" + entity + " not allowed by checker!");
}
}
}
}
private static <T extends AbstractJpaBaseEntity> Specification<T> nop() {
return (targetRoot, query, cb) -> cb.equal(targetRoot.get(AbstractJpaBaseEntity_.id), -1);
}
private record AccessRuleId<T>(Class<T> ruleClass, AccessController.Operation operation) {}
private record AccessRule<T>(Specification<T> specification, Predicate<T> checker) {}
}

View File

@@ -253,8 +253,6 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
/**
* Test method for {@link org.eclipse.hawkbit.repository.ArtifactManagement#delete(long)}.
*/
/**
* Tests the deletion of a local artifact including metadata.
*/
@Test
@@ -585,7 +583,7 @@ class ArtifactManagementTest extends AbstractJpaIntegrationTest {
}
private <T> T runAsTenant(final String tenant, final Callable<T> callable) throws Exception {
return SecurityContextSwitch.runAs(SecurityContextSwitch.withUserAndTenantAllSpPermissions("user", tenant), callable);
return SecurityContextSwitch.callAs(SecurityContextSwitch.withUserAndTenantAllSpPermissions("user", tenant), callable);
}
private SoftwareModule createSoftwareModuleForTenant(final String tenant) throws Exception {

View File

@@ -16,6 +16,7 @@ import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpre
import static org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS;
import static org.eclipse.hawkbit.repository.jpa.configuration.Constants.TX_RT_MAX;
import static org.eclipse.hawkbit.repository.model.Action.ActionType.DOWNLOAD_ONLY;
import static org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch.runAs;
import static org.eclipse.hawkbit.repository.test.util.TestdataFactory.DEFAULT_CONTROLLER_ID;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
@@ -107,11 +108,9 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
testdataFactory.createTarget(controllerId);
final WithUser withController = SecurityContextSwitch.withController("controller", CONTROLLER_ROLE_ANONYMOUS);
assertThatExceptionOfType(AssignmentQuotaExceededException.class).isThrownBy(() -> SecurityContextSwitch
.runAs(withController, () -> {
writeAttributes(controllerId, allowedAttributes + 1, "key", "value");
return null;
})).withMessageContaining("" + allowedAttributes);
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> runAs(withController, () -> writeAttributes(controllerId, allowedAttributes + 1, "key", "value")))
.withMessageContaining("" + allowedAttributes);
// verify that no attributes have been written
assertThat(targetManagement.getControllerAttributes(controllerId)).isEmpty();
@@ -121,13 +120,12 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
SecurityContextSwitch.runAs(withController, () -> {
writeAttributes(controllerId, allowedAttributes, "key", "value1");
writeAttributes(controllerId, allowedAttributes, "key", "value2");
return null;
});
assertThat(targetManagement.getControllerAttributes(controllerId)).hasSize(10);
// Now rite one more
assertThatExceptionOfType(AssignmentQuotaExceededException.class).isThrownBy(() -> SecurityContextSwitch
.runAs(withController, () -> {
.getAs(withController, () -> {
writeAttributes(controllerId, 1, "additional", "value1");
return null;
})).withMessageContaining("" + allowedAttributes);
@@ -185,7 +183,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
final Long actionId = createTargetAndAssignDs();
SecurityContextSwitch
.runAs(SecurityContextSwitch.withController("controller", CONTROLLER_ROLE_ANONYMOUS), () -> {
.getAs(SecurityContextSwitch.withController("controller", CONTROLLER_ROLE_ANONYMOUS), () -> {
// Fails as one entry is already in there from the assignment
assertThatExceptionOfType(AssignmentQuotaExceededException.class)
.isThrownBy(() -> writeStatus(actionId, allowStatusEntries))
@@ -345,8 +343,8 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
}
/**
* Verifies that management queries react as specified on calls for non existing entities
* by means of throwing EntityNotFoundException.
* Verifies that management queries react as specified on calls for non existing entities
* by means of throwing EntityNotFoundException.
*/
@Test
@ExpectEvents({
@@ -571,7 +569,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
}
/**
* Controller rejects action cancellation with CANCEL_REJECTED status. Action goes back to RUNNING status as it expects
* Controller rejects action cancellation with CANCEL_REJECTED status. Action goes back to RUNNING status as it expects
* that the controller will continue the original update.
*/
@Test
@@ -639,8 +637,8 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
}
/**
* Verifies that assignment verification works based on SHA1 hash. By design it is not important which artifact
* is actually used for the check as long as they have an identical binary, i.e. same SHA1 hash.
* Verifies that assignment verification works based on SHA1 hash. By design it is not important which artifact
* is actually used for the check as long as they have an identical binary, i.e. same SHA1 hash.
*/
@Test
@ExpectEvents({
@@ -878,7 +876,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
}
/**
* Register a controller which does not exist, when a ConcurrencyFailureException is raised, the
* Register a controller which does not exist, when a ConcurrencyFailureException is raised, the
* exception is not rethrown when the max retries are not yet reached
*/
@Test
@@ -957,7 +955,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
}
/**
* Retry is aborted when an unchecked exception is thrown and the exception should also be
* Retry is aborted when an unchecked exception is thrown and the exception should also be
* rethrown
*/
@Test
@@ -1004,7 +1002,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
@Test
@ExpectEvents({ @Expect(type = TargetCreatedEvent.class, count = 1) })
@SuppressWarnings("java:S2699")
// java:S2699 - test tests the fired events, no need for assert
// java:S2699 - test tests the fired events, no need for assert
void targetPollEventNotSendIfDisabled() {
repositoryProperties.setPublishTargetPollEvent(false);
controllerManagement.findOrRegisterTargetIfItDoesNotExist("AA", LOCALHOST);
@@ -1121,7 +1119,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
}
/**
* Controller tries to send an update feedback after it has been finished which is accepted as the repository is
* Controller tries to send an update feedback after it has been finished which is accepted as the repository is
* configured to accept them.
*/
@Test
@@ -1160,7 +1158,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
final String controllerId = "test123";
final Target target = testdataFactory.createTarget(controllerId);
SecurityContextSwitch.runAs(SecurityContextSwitch.withController(
SecurityContextSwitch.getAs(SecurityContextSwitch.withController(
"controller",
CONTROLLER_ROLE_ANONYMOUS, SpPermission.READ_TARGET), () -> {
addAttributeAndVerify(controllerId);
@@ -1289,7 +1287,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
}
/**
* Verifies that quota is asserted when a controller reports too many DOWNLOADED events for a
* Verifies that quota is asserted when a controller reports too many DOWNLOADED events for a
* DOWNLOAD_ONLY action.
*/
@Test
@@ -1516,7 +1514,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
}
/**
* Verifies that a target can report FINISHED/ERROR updates for DOWNLOAD_ONLY assignments regardless of
* Verifies that a target can report FINISHED/ERROR updates for DOWNLOAD_ONLY assignments regardless of
* repositoryProperties.rejectActionStatusForClosedAction value.
*/
@Test
@@ -1565,7 +1563,7 @@ class ControllerManagementTest extends AbstractJpaIntegrationTest {
/**
* Verifies that a controller can report a FINISHED event for a DOWNLOAD_ONLY action after having
* installed an intermediate update.
* installed an intermediate update.
*/
@Test
@ExpectEvents({

View File

@@ -1442,22 +1442,22 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
// create scheduled rollout fails without handle rollout permission
assertThatExceptionOfType(InsufficientPermissionException.class)
.as("Insufficient permission exception when startAt and no handle rollout permission")
.isThrownBy(() -> SecurityContextSwitch.runAs(
.isThrownBy(() -> SecurityContextSwitch.getAs(
userWithoutHandleRollout, () -> createRolloutWithStartAt(rolloutName, filter, distributionSet, 1L)));
// same action succeeds with handle rollout permission
SecurityContextSwitch.runAs(
SecurityContextSwitch.getAs(
userWithHandleRollout,
() -> createRolloutWithStartAt(rolloutName + "_withStartTime", filter, distributionSet, 1L));
// same action succeeds with system role permission
SecurityContextSwitch.runAs(
SecurityContextSwitch.getAs(
userWithSystemRole,
() -> createRolloutWithStartAt(rolloutName + "_withStartTimeSystemRole", filter, distributionSet, 1L));
// same action succeeds without handle rollout permission but with null start at
SecurityContextSwitch.runAs(
SecurityContextSwitch.getAs(
userWithoutHandleRollout,
() -> createRolloutWithStartAt(rolloutName + "_withoutStartTime", filter, distributionSet, null));
// same action succeeds without handle rollout permission but with Long.MAX_VALUE start at
SecurityContextSwitch.runAs(
SecurityContextSwitch.getAs(
userWithoutHandleRollout,
() -> createRolloutWithStartAt(rolloutName + "_withLongMax", filter, distributionSet, Long.MAX_VALUE));
}
@@ -2501,7 +2501,7 @@ class RolloutManagementTest extends AbstractJpaIntegrationTest {
.pollInterval(Duration.ofMillis(500))
.atMost(Duration.ofSeconds(10))
.until(() -> SecurityContextSwitch
.runAsPrivileged(
.callAsPrivileged(
() -> rolloutManagement.get(myRolloutId).orElseThrow(NoSuchElementException::new))
.getStatus().equals(RolloutStatus.RUNNING));
}

View File

@@ -137,7 +137,7 @@ class SystemManagementTest extends AbstractJpaIntegrationTest {
for (int i = 0; i < tenants; i++) {
final String tenantname = "TENANT" + i;
SecurityContextSwitch.runAs(SecurityContextSwitch.withUserAndTenant("bumlux", tenantname, true, true, false,
SecurityContextSwitch.getAs(SecurityContextSwitch.withUserAndTenant("bumlux", tenantname, true, true, false,
SpringEvalExpressions.SYSTEM_ROLE), () -> {
systemManagement.getTenantMetadataWithoutDetails();
if (artifactSize > 0) {

View File

@@ -167,15 +167,15 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
.create(entityFactory.target().create().controllerId("targetWithSecurityToken").securityToken("token"));
// retrieve security token only with READ_TARGET_SEC_TOKEN permission
final String securityTokenWithReadPermission = SecurityContextSwitch.runAs(
final String securityTokenWithReadPermission = SecurityContextSwitch.getAs(
SecurityContextSwitch.withUser("OnlyTargetReadPermission", SpPermission.READ_TARGET_SEC_TOKEN),
createdTarget::getSecurityToken);
// retrieve security token only with ROLE_TARGET_ADMIN permission
final String securityTokenWithTargetAdminPermission = SecurityContextSwitch.runAs(
final String securityTokenWithTargetAdminPermission = SecurityContextSwitch.getAs(
SecurityContextSwitch.withUser("OnlyTargetAdminPermission", SpRole.TARGET_ADMIN),
createdTarget::getSecurityToken);
// retrieve security token only with ROLE_TENANT_ADMIN permission
final String securityTokenWithTenantAdminPermission = SecurityContextSwitch.runAs(
final String securityTokenWithTenantAdminPermission = SecurityContextSwitch.getAs(
SecurityContextSwitch.withUser("OnlyTenantAdminPermission", SpRole.TENANT_ADMIN),
createdTarget::getSecurityToken);
@@ -184,7 +184,7 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
// retrieve security token without any permissions
final String securityTokenWithoutPermission = SecurityContextSwitch
.runAs(SecurityContextSwitch.withUser("NoPermission"), createdTarget::getSecurityToken);
.getAs(SecurityContextSwitch.withUser("NoPermission"), createdTarget::getSecurityToken);
assertThat(createdTarget.getSecurityToken()).isEqualTo("token");
assertThat(securityTokenWithReadPermission).isNotNull();
@@ -705,7 +705,7 @@ class TargetManagementTest extends AbstractJpaIntegrationTest {
final String knownTargetControllerId = "readTarget";
controllerManagement.findOrRegisterTargetIfItDoesNotExist(knownTargetControllerId, new URI("http://127.0.0.1"));
SecurityContextSwitch.runAs(SecurityContextSwitch.withUser("bumlux", "READ_TARGET"), () -> {
SecurityContextSwitch.getAs(SecurityContextSwitch.withUser("bumlux", "READ_TARGET"), () -> {
final Target findTargetByControllerID = targetManagement.getByControllerID(knownTargetControllerId)
.orElseThrow(IllegalStateException::new);
assertThat(findTargetByControllerID).isNotNull();

View File

@@ -19,7 +19,7 @@ import jakarta.persistence.PersistenceContext;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.TargetFields;
import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration;
import org.eclipse.hawkbit.repository.jpa.JpaRepositoryConfiguration;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.ql.utils.HawkbitQlToSql;
import org.eclipse.hawkbit.repository.test.TestConfiguration;
@@ -35,7 +35,7 @@ import org.springframework.test.context.ContextConfiguration;
"spring.main.allow-bean-definition-overriding=true",
"spring.main.banner-mode=off",
"logging.level.root=ERROR" })
@ContextConfiguration(classes = { RepositoryApplicationConfiguration.class, TestConfiguration.class })
@ContextConfiguration(classes = { JpaRepositoryConfiguration.class, TestConfiguration.class })
@Disabled("For manual run only, while playing around with RSQL to SQL")
@SuppressWarnings("java:S2699") // java:S2699 - manual test, don't actually does assertions
class RsqlToSqlTest {

View File

@@ -113,13 +113,13 @@ class MultiTenancyEntityTest extends AbstractJpaIntegrationTest {
// logged in tenant mytenant - check if tenant default data is
// autogenerated
assertThat(distributionSetTypeManagement.findAll(PAGE)).isEmpty();
SecurityContextSwitch.runAsPrivileged(() ->
SecurityContextSwitch.callAsPrivileged(() ->
assertThat(systemManagement.createTenantMetadata("mytenant").getTenant().toUpperCase()).isEqualTo("mytenant".toUpperCase()));
assertThat(distributionSetTypeManagement.findAll(PAGE)).isNotEmpty();
// check that the cache is not getting in the way, i.e. "bumlux" results in bumlux and not mytenant
assertThat(SecurityContextSwitch.runAs(
assertThat(SecurityContextSwitch.getAs(
SecurityContextSwitch.withUserAndTenantAllSpPermissions("user", "bumlux"),
() -> systemManagement.getTenantMetadataWithoutDetails().getTenant().toUpperCase()))
.isEqualTo("bumlux".toUpperCase());
@@ -178,7 +178,7 @@ class MultiTenancyEntityTest extends AbstractJpaIntegrationTest {
}
private <T> T runAsTenant(final String tenant, final Callable<T> callable) throws Exception {
return SecurityContextSwitch.runAs(SecurityContextSwitch.withUserAndTenantAllSpPermissions("user", tenant), callable);
return SecurityContextSwitch.callAs(SecurityContextSwitch.withUserAndTenantAllSpPermissions("user", tenant), callable);
}
private Target createTargetForTenant(final String controllerId, final String tenant) throws Exception {

View File

@@ -54,4 +54,7 @@ logging.level.org.eclipse.persistence=ERROR
hawkbit.repository.cluster.lock.ttl=1000
hawkbit.repository.cluster.lock.refreshOnRemainMS=200
hawkbit.repository.cluster.lock.refreshOnRemainPercent=10
hawkbit.repository.cluster.lock.ticPeriodMS=10
# reduce scheduler tic period to speed up tests
hawkbit.repository.cluster.lock.ticPeriodMS=10
# disable spring cloud bus for tests
spring.cloud.bus.enabled=false