Fix EntityMatcher when for Identifiable.getId (#2724)
* Fix EntityMatcher to process properly filters of type targetType.id - to resolve correctly the getter return type Long not T * Add AutoAsssignTest access control test * Simplify rest of the ACM tests Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
2
.github/workflows/first-interaction.yaml
vendored
2
.github/workflows/first-interaction.yaml
vendored
@@ -17,6 +17,8 @@ jobs:
|
||||
- uses: actions/first-interaction@v3
|
||||
with:
|
||||
repo_token: ${{ secrets.PAT_SECRET }}
|
||||
issue_message: |-
|
||||
Thanks @${{ github.actor }} for submitting an issue to hawkBit!! Make yourself comfortable while I'm looking for a committer to assist you.
|
||||
pr_message: |-
|
||||
Thanks @${{ github.actor }} for taking the time to contribute to hawkBit! We really appreciate this. Make yourself comfortable while I'm looking for a committer to help you with your contribution.
|
||||
Please make sure you read the [contribution guide](https://github.com/eclipse-hawkbit/hawkbit/blob/master/CONTRIBUTING.md) and signed the Eclipse Contributor Agreement (ECA).
|
||||
@@ -61,12 +61,5 @@ jobs:
|
||||
- name: Check file license headers
|
||||
run: mvn license:check -PcheckLicense --batch-mode
|
||||
|
||||
- name: Check code style
|
||||
run: |
|
||||
# compare style to target branch
|
||||
git remote add target ${{ github.event.pull_request.base.repo.clone_url }}
|
||||
git fetch target
|
||||
mvn spotless:check -DratchetFrom=${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Run tests & javadoc
|
||||
run: mvn clean verify javadoc:javadoc -DdetectOfflineLinks=false -PgenerateTestReport ${{ inputs.maven_properties }} --batch-mode
|
||||
36
.github/workflows/style_check.yaml
vendored
Normal file
36
.github/workflows/style_check.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Style Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.3rd-party/**'
|
||||
- 'site/**'
|
||||
- '**.md'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
style_check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
repository: ${{ inputs.repository }}
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 21
|
||||
cache: 'maven'
|
||||
|
||||
- name: Check code style
|
||||
run: |
|
||||
# compare style to target branch
|
||||
git remote add target ${{ github.event.pull_request.base.repo.clone_url }}
|
||||
git fetch target
|
||||
mvn spotless:check -DratchetFrom=${{ github.event.pull_request.base.sha }}
|
||||
@@ -31,6 +31,7 @@ import java.util.Objects;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
import org.eclipse.hawkbit.repository.jpa.ql.Node.Comparison.Operator;
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
||||
/**
|
||||
* Provides entity matcher that matches an entity object against a filter (a {@link Node} or an RSQL string).
|
||||
@@ -57,7 +58,7 @@ public class EntityMatcher {
|
||||
return match(t, root);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"java:S3776", "java:S3358", "java:S1125", "java:S6541"}) // better readable this way
|
||||
@SuppressWarnings({ "java:S3776", "java:S3358", "java:S1125", "java:S6541" }) // better readable this way
|
||||
private <T> boolean match(final T t, final Node node) {
|
||||
if (node instanceof Node.Comparison comparison) {
|
||||
final String[] split = comparison.getKey().split("\\.", 2);
|
||||
@@ -89,7 +90,8 @@ public class EntityMatcher {
|
||||
value = map(comparison.getValue(), getReturnType(valueGetter));
|
||||
compare = (e, operator) -> {
|
||||
try {
|
||||
return compareIgnoreCaseAware(map(e == null ? null : valueGetter.get(e), getReturnType(valueGetter)), operator, value);
|
||||
return compareIgnoreCaseAware(
|
||||
map(e == null ? null : valueGetter.get(e), getReturnType(valueGetter)), operator, value);
|
||||
} catch (final IllegalAccessException | InvocationTargetException ex) {
|
||||
throw new IllegalArgumentException(ex);
|
||||
}
|
||||
@@ -143,6 +145,7 @@ public class EntityMatcher {
|
||||
private boolean compareIgnoreCaseAware(final Object entityValue, final Operator op, final Object comparisonValue) {
|
||||
return compare(ignoreCase(entityValue), op, ignoreCase(comparisonValue));
|
||||
}
|
||||
|
||||
private Object ignoreCase(final Object o) {
|
||||
if (!ignoreCase || o == null) {
|
||||
return o;
|
||||
@@ -157,7 +160,9 @@ public class EntityMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("java:S3011") // java:S3011 uses reflection to private members anyway
|
||||
// java:S3011 uses reflection to private members anyway
|
||||
// java:S3358 - better readable this way
|
||||
@SuppressWarnings({ "java:S3011", "java:S3358" })
|
||||
private static <T> Getter getGetter(final Class<T> t, final String fieldName) throws NoSuchMethodException {
|
||||
final String[] parts = fieldName.split("\\.");
|
||||
if (parts.length > 1) {
|
||||
@@ -197,7 +202,12 @@ public class EntityMatcher {
|
||||
|
||||
@Override
|
||||
public Type type() {
|
||||
return getter.getGenericReturnType();
|
||||
final Type type = getter.getGenericReturnType();
|
||||
return type instanceof Class<?>
|
||||
? type
|
||||
: type instanceof ParameterizedType
|
||||
? type // Map or Collection generic type
|
||||
: ResolvableType.forMethodReturnType(getter, t).resolve();
|
||||
}
|
||||
};
|
||||
} catch (final NoSuchMethodException e) {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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 java.util.Set;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetFields;
|
||||
import org.eclipse.hawkbit.repository.DistributionSetTypeFields;
|
||||
import org.eclipse.hawkbit.repository.SoftwareModuleFields;
|
||||
import org.eclipse.hawkbit.repository.SoftwareModuleTypeFields;
|
||||
import org.eclipse.hawkbit.repository.TargetFields;
|
||||
import org.eclipse.hawkbit.repository.TargetTagFields;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
||||
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport;
|
||||
|
||||
// utility class to validate authorities when ACM is enabled
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class AuthorityChecker {
|
||||
|
||||
private static final Set<String> ALL_AUTHORITIES = SpPermission.getAllTenantAuthorities();
|
||||
|
||||
public static String[] validateAuthorities(final String... authorities) {
|
||||
for (final String authority : authorities) {
|
||||
validateAuthority(authority);
|
||||
}
|
||||
return authorities;
|
||||
}
|
||||
|
||||
public static void validateAuthority(final String authority) {
|
||||
final int index = authority.indexOf('/');
|
||||
final String unscopedPermission = index > 0 ? authority.substring(0, index) : authority;
|
||||
if (index > 0) {
|
||||
validateScope(group(unscopedPermission), authority.substring(index + 1), authority);
|
||||
}
|
||||
if (!ALL_AUTHORITIES.contains(unscopedPermission)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unknown permission: " + unscopedPermission + (index > 0 ? " (unscoped of " + authority + ")" : ""));
|
||||
}
|
||||
}
|
||||
|
||||
private static String group(final String unscopedPermission) {
|
||||
if (unscopedPermission.startsWith(SpPermission.CREATE_PREFIX)) {
|
||||
return unscopedPermission.substring(SpPermission.CREATE_PREFIX.length());
|
||||
} else if (unscopedPermission.startsWith(SpPermission.READ_PREFIX)) {
|
||||
return unscopedPermission.substring(SpPermission.READ_PREFIX.length());
|
||||
} else if (unscopedPermission.startsWith(SpPermission.UPDATE_PREFIX)) {
|
||||
return unscopedPermission.substring(SpPermission.UPDATE_PREFIX.length());
|
||||
} else if (unscopedPermission.startsWith(SpPermission.DELETE_PREFIX)) {
|
||||
return unscopedPermission.substring(SpPermission.DELETE_PREFIX.length());
|
||||
} else {
|
||||
throw new IllegalArgumentException(unscopedPermission + " doesn't support targetTypeScope");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private static void validateScope(final String permission, final String rsql, final String authority) {
|
||||
// validate RSQL
|
||||
final Class<?> rsqlQueryFieldType;
|
||||
final Class<?> jpaType;
|
||||
switch (permission) {
|
||||
case SpPermission.TARGET -> {
|
||||
rsqlQueryFieldType = TargetFields.class;
|
||||
jpaType = JpaTarget.class;
|
||||
}
|
||||
case SpPermission.TARGET_TYPE -> {
|
||||
rsqlQueryFieldType = TargetTagFields.class;
|
||||
jpaType = JpaTarget.class;
|
||||
}
|
||||
case SpPermission.SOFTWARE_MODULE -> {
|
||||
rsqlQueryFieldType = SoftwareModuleFields.class;
|
||||
jpaType = JpaSoftwareModule.class;
|
||||
}
|
||||
case SpPermission.SOFTWARE_MODULE_TYPE -> {
|
||||
rsqlQueryFieldType = SoftwareModuleTypeFields.class;
|
||||
jpaType = JpaSoftwareModuleType.class;
|
||||
}
|
||||
case SpPermission.DISTRIBUTION_SET -> {
|
||||
rsqlQueryFieldType = DistributionSetFields.class;
|
||||
jpaType = JpaDistributionSet.class;
|
||||
}
|
||||
case SpPermission.DISTRIBUTION_SET_TYPE -> {
|
||||
rsqlQueryFieldType = DistributionSetTypeFields.class;
|
||||
jpaType = JpaTarget.class;
|
||||
}
|
||||
default -> throw new IllegalArgumentException(permission + " doesn't support targetTypeScope");
|
||||
}
|
||||
try {
|
||||
QLSupport.getInstance().validate(rsql, (Class) rsqlQueryFieldType, jpaType);
|
||||
} catch (final RuntimeException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Scope of " + authority + " is not a valid RSQL for " + permission + ": " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 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.autoassign;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.ContextAware;
|
||||
import org.eclipse.hawkbit.repository.DeploymentManagement;
|
||||
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
|
||||
import org.eclipse.hawkbit.repository.autoassign.AutoAssignExecutor;
|
||||
import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
|
||||
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.Isolation;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Abstract implementation of an AutoAssignExecutor
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractAutoAssignExecutor implements AutoAssignExecutor {
|
||||
|
||||
/**
|
||||
* The message which is added to the action status when a distribution set is
|
||||
* assigned to an target. First %s is the name of the target filter.
|
||||
*/
|
||||
private static final String ACTION_MESSAGE = "Auto assignment by target filter: %s";
|
||||
|
||||
/**
|
||||
* Maximum for target filter queries with auto assign DS activated.
|
||||
*/
|
||||
private static final int PAGE_SIZE = 1000;
|
||||
|
||||
private final TargetFilterQueryManagement<? extends TargetFilterQuery> targetFilterQueryManagement;
|
||||
private final DeploymentManagement deploymentManagement;
|
||||
private final PlatformTransactionManager transactionManager;
|
||||
private final ContextAware contextAware;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param targetFilterQueryManagement to get all target filter queries
|
||||
* @param deploymentManagement to assign distribution sets to targets
|
||||
* @param transactionManager to run transactions
|
||||
* @param contextAware to handle the context
|
||||
*/
|
||||
protected AbstractAutoAssignExecutor(
|
||||
final TargetFilterQueryManagement<? extends TargetFilterQuery> targetFilterQueryManagement,
|
||||
final DeploymentManagement deploymentManagement,
|
||||
final PlatformTransactionManager transactionManager, final ContextAware contextAware) {
|
||||
this.targetFilterQueryManagement = targetFilterQueryManagement;
|
||||
this.deploymentManagement = deploymentManagement;
|
||||
this.transactionManager = transactionManager;
|
||||
this.contextAware = contextAware;
|
||||
}
|
||||
|
||||
protected static String getAutoAssignmentInitiatedBy(final TargetFilterQuery targetFilterQuery) {
|
||||
return StringUtils.hasText(targetFilterQuery.getAutoAssignInitiatedBy())
|
||||
? targetFilterQuery.getAutoAssignInitiatedBy()
|
||||
: targetFilterQuery.getCreatedBy();
|
||||
}
|
||||
|
||||
protected DeploymentManagement getDeploymentManagement() {
|
||||
return deploymentManagement;
|
||||
}
|
||||
|
||||
protected PlatformTransactionManager getTransactionManager() {
|
||||
return transactionManager;
|
||||
}
|
||||
|
||||
protected TenantAware getContextAware() {
|
||||
return contextAware;
|
||||
}
|
||||
|
||||
// run in the context the auto assignment is made in, i.e. if there is access control context it runs in it
|
||||
// otherwise in the tenant & user context built by createdBy
|
||||
// Note! It must be called in a tenant context, i.e. contextAware.getCurrentTenant() returns the tenant
|
||||
protected void forEachFilterWithAutoAssignDS(final Consumer<TargetFilterQuery> consumer) {
|
||||
Slice<TargetFilterQuery> filterQueries;
|
||||
Pageable query = PageRequest.of(0, PAGE_SIZE);
|
||||
|
||||
do {
|
||||
filterQueries = targetFilterQueryManagement.findWithAutoAssignDS(query);
|
||||
|
||||
filterQueries.forEach(filterQuery -> {
|
||||
try {
|
||||
filterQuery.getAccessControlContext().ifPresentOrElse(
|
||||
context -> // has stored context - executes it with it
|
||||
contextAware.runInContext(
|
||||
context,
|
||||
() -> consumer.accept(filterQuery)),
|
||||
() -> // has no stored context - executes it in the tenant & user scope
|
||||
contextAware.runAsTenantAsUser(
|
||||
contextAware.getCurrentTenant(),
|
||||
getAutoAssignmentInitiatedBy(filterQuery), () -> {
|
||||
consumer.accept(filterQuery);
|
||||
return null;
|
||||
})
|
||||
);
|
||||
} catch (final RuntimeException ex) {
|
||||
log.debug(
|
||||
"Exception on forEachFilterWithAutoAssignDS execution for tenant {} with filter id {}. Continue with next filter query.",
|
||||
filterQuery.getTenant(), filterQuery.getId(), ex);
|
||||
log.error(
|
||||
"Exception on forEachFilterWithAutoAssignDS execution for tenant {} with filter id {} and error message [{}]. "
|
||||
+ "Continue with next filter query.",
|
||||
filterQuery.getTenant(), filterQuery.getId(), ex.getMessage());
|
||||
}
|
||||
});
|
||||
} while ((query = filterQueries.nextPageable()) != Pageable.unpaged());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs target assignments within a dedicated transaction for a given list of
|
||||
* controllerIDs
|
||||
*
|
||||
* @param targetFilterQuery the target filter query
|
||||
* @param controllerIds the controllerIDs
|
||||
* @return count of targets
|
||||
*/
|
||||
protected int runTransactionalAssignment(final TargetFilterQuery targetFilterQuery,
|
||||
final List<String> controllerIds) {
|
||||
final String actionMessage = String.format(ACTION_MESSAGE, targetFilterQuery.getName());
|
||||
|
||||
return DeploymentHelper.runInNewTransaction(getTransactionManager(), "autoAssignDSToTargets",
|
||||
Isolation.READ_COMMITTED.value(), status -> {
|
||||
|
||||
final List<DeploymentRequest> deploymentRequests = mapToDeploymentRequests(controllerIds,
|
||||
targetFilterQuery);
|
||||
|
||||
final int count = deploymentRequests.size();
|
||||
if (count > 0) {
|
||||
getDeploymentManagement().assignDistributionSets(
|
||||
getAutoAssignmentInitiatedBy(targetFilterQuery), deploymentRequests, actionMessage);
|
||||
}
|
||||
return count;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of {@link DeploymentRequest} for given list of controllerIds
|
||||
* and {@link TargetFilterQuery}
|
||||
*
|
||||
* @param controllerIds list of controllerIds
|
||||
* @param filterQuery the query the targets have to match
|
||||
* @return list of deployment request
|
||||
*/
|
||||
protected List<DeploymentRequest> mapToDeploymentRequests(final List<String> controllerIds,
|
||||
final TargetFilterQuery filterQuery) {
|
||||
// the action type is set to FORCED per default (when not explicitly
|
||||
// specified)
|
||||
final Action.ActionType autoAssignActionType = filterQuery.getAutoAssignActionType() == null
|
||||
? Action.ActionType.FORCED
|
||||
: filterQuery.getAutoAssignActionType();
|
||||
|
||||
return controllerIds.stream()
|
||||
.map(controllerId -> DeploymentRequest
|
||||
.builder(controllerId, filterQuery.getAutoAssignDistributionSet().getId())
|
||||
.actionType(autoAssignActionType).weight(filterQuery.getAutoAssignWeight().orElse(null))
|
||||
.confirmationRequired(filterQuery.isConfirmationRequired()).build())
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ package org.eclipse.hawkbit.repository.jpa.autoassign;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.persistence.PersistenceException;
|
||||
|
||||
@@ -20,109 +21,195 @@ import org.eclipse.hawkbit.exception.AbstractServerRtException;
|
||||
import org.eclipse.hawkbit.repository.DeploymentManagement;
|
||||
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
|
||||
import org.eclipse.hawkbit.repository.TargetManagement;
|
||||
import org.eclipse.hawkbit.repository.autoassign.AutoAssignExecutor;
|
||||
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
|
||||
import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper;
|
||||
import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.DeploymentRequest;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Slice;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.Isolation;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Checks if targets need a new distribution set (DS) based on the target filter
|
||||
* queries and assigns the new DS when necessary. First all target filter
|
||||
* queries are listed. For every target filter query (TFQ) the auto assign DS is
|
||||
* retrieved. All targets get listed per target filter query, that match the TFQ
|
||||
* and that don't have the auto assign DS in their action history.
|
||||
* Checks if targets need a new distribution set (DS) based on the target filter queries and assigns the new DS when necessary. First all target
|
||||
* filter queries are listed. For every target filter query (TFQ) the auto assign DS is retrieved. All targets get listed per target filter
|
||||
* query, that match the TFQ and that don't have the auto assign DS in their action history.
|
||||
*/
|
||||
@Slf4j
|
||||
public class AutoAssignChecker extends AbstractAutoAssignExecutor {
|
||||
|
||||
private final TargetManagement<? extends Target> targetManagement;
|
||||
public class AutoAssignChecker implements AutoAssignExecutor {
|
||||
|
||||
/**
|
||||
* Instantiates a new auto assign checker
|
||||
*
|
||||
* @param targetFilterQueryManagement to get all target filter queries
|
||||
* @param targetManagement to get targets
|
||||
* @param deploymentManagement to assign distribution sets to targets
|
||||
* @param transactionManager to run transactions
|
||||
* @param contextAware to handle the context
|
||||
* The message which is added to the action status when a distribution set is assigned to a target.
|
||||
* First %s is the name of the target filter.
|
||||
*/
|
||||
private static final String ACTION_MESSAGE = "Auto assignment by target filter: %s";
|
||||
|
||||
/**
|
||||
* Maximum for target filter queries with auto assign DS activated.
|
||||
*/
|
||||
private static final int PAGE_SIZE = 1000;
|
||||
|
||||
private final TargetFilterQueryManagement<? extends TargetFilterQuery> targetFilterQueryManagement;
|
||||
private final TargetManagement<? extends Target> targetManagement;
|
||||
private final DeploymentManagement deploymentManagement;
|
||||
private final PlatformTransactionManager transactionManager;
|
||||
private final ContextAware contextAware;
|
||||
|
||||
public AutoAssignChecker(
|
||||
final TargetFilterQueryManagement<? extends TargetFilterQuery> targetFilterQueryManagement,
|
||||
final TargetManagement<? extends Target> targetManagement, final DeploymentManagement deploymentManagement,
|
||||
final PlatformTransactionManager transactionManager, final ContextAware contextAware) {
|
||||
super(targetFilterQueryManagement, deploymentManagement, transactionManager, contextAware);
|
||||
this.targetFilterQueryManagement = targetFilterQueryManagement;
|
||||
this.targetManagement = targetManagement;
|
||||
this.deploymentManagement = deploymentManagement;
|
||||
this.transactionManager = transactionManager;
|
||||
this.contextAware = contextAware;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void checkAllTargets() {
|
||||
log.debug("Auto assign check call for tenant {} started", getContextAware().getCurrentTenant());
|
||||
log.debug("Auto assign check call started");
|
||||
forEachFilterWithAutoAssignDS(this::checkByTargetFilterQueryAndAssignDS);
|
||||
log.debug("Auto assign check call for tenant {} finished", getContextAware().getCurrentTenant());
|
||||
log.debug("Auto assign check call finished");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkSingleTarget(String controllerId) {
|
||||
log.debug("Auto assign check call for tenant {} and device {} started", getContextAware().getCurrentTenant(), controllerId);
|
||||
log.debug("Auto assign check call for device {} started", controllerId);
|
||||
forEachFilterWithAutoAssignDS(filter -> checkForDevice(controllerId, filter));
|
||||
log.debug("Auto assign check call for tenant {} and device {} finished", getContextAware().getCurrentTenant(), controllerId);
|
||||
log.debug("Auto assign check call for device {} finished", controllerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the distribution set, gets all controllerIds and assigns the DS to
|
||||
* them. Catches PersistenceException and own exceptions derived from
|
||||
* AbstractServerRtException
|
||||
* Fetches the distribution set, gets all controllerIds and assigns the DS to them. Catches PersistenceException and own exceptions derived
|
||||
* from AbstractServerRtException
|
||||
*
|
||||
* @param targetFilterQuery the target filter query
|
||||
*/
|
||||
private void checkByTargetFilterQueryAndAssignDS(final TargetFilterQuery targetFilterQuery) {
|
||||
log.debug("Auto assign check call for tenant {} and target filter query id {} started",
|
||||
getContextAware().getCurrentTenant(), targetFilterQuery.getId());
|
||||
log.debug("Auto assign check call for target filter query id {} started", targetFilterQuery.getId());
|
||||
try {
|
||||
int count;
|
||||
do {
|
||||
final List<String> controllerIds = targetManagement
|
||||
.findByTargetFilterQueryAndNonDSAndCompatibleAndUpdatable(
|
||||
targetFilterQuery.getAutoAssignDistributionSet().getId(), targetFilterQuery.getQuery(),
|
||||
PageRequest.of(0, Constants.MAX_ENTRIES_IN_STATEMENT)
|
||||
)
|
||||
PageRequest.of(0, Constants.MAX_ENTRIES_IN_STATEMENT))
|
||||
.getContent().stream().map(Target::getControllerId).toList();
|
||||
log.debug(
|
||||
"Retrieved {} auto assign targets for tenant {} and target filter query id {}, starting with assignment",
|
||||
controllerIds.size(), getContextAware().getCurrentTenant(), targetFilterQuery.getId());
|
||||
log.debug("Retrieved {} auto assign targets for target filter query id {}, starting with assignment",
|
||||
controllerIds.size(), targetFilterQuery.getId());
|
||||
|
||||
count = runTransactionalAssignment(targetFilterQuery, controllerIds);
|
||||
log.debug(
|
||||
"Assignment for {} auto assign targets for tenant {} and target filter query id {} finished",
|
||||
controllerIds.size(), getContextAware().getCurrentTenant(), targetFilterQuery.getId());
|
||||
log.debug("Assignment for {} auto assign targets for target filter query id {} finished",
|
||||
controllerIds.size(), targetFilterQuery.getId());
|
||||
} while (count == Constants.MAX_ENTRIES_IN_STATEMENT);
|
||||
} catch (final PersistenceException | AbstractServerRtException e) {
|
||||
log.error("Error during auto assign check of target filter query id {}", targetFilterQuery.getId(), e);
|
||||
}
|
||||
log.debug("Auto assign check call for tenant {} and target filter query id {} finished",
|
||||
getContextAware().getCurrentTenant(), targetFilterQuery.getId());
|
||||
log.debug("Auto assign check call for target filter query id {} finished", targetFilterQuery.getId());
|
||||
}
|
||||
|
||||
private static String getAutoAssignmentInitiatedBy(final TargetFilterQuery targetFilterQuery) {
|
||||
return StringUtils.hasText(targetFilterQuery.getAutoAssignInitiatedBy())
|
||||
? targetFilterQuery.getAutoAssignInitiatedBy()
|
||||
: targetFilterQuery.getCreatedBy();
|
||||
}
|
||||
|
||||
// run in the context the auto assignment is made in, i.e. if there is access control context it runs in it
|
||||
// otherwise in the tenant & user context built by createdBy
|
||||
// Note: It must be called in a tenant context, i.e. contextAware.getCurrentTenant() returns the tenant
|
||||
private void forEachFilterWithAutoAssignDS(final Consumer<TargetFilterQuery> consumer) {
|
||||
Slice<TargetFilterQuery> filterQueries;
|
||||
Pageable query = PageRequest.of(0, PAGE_SIZE);
|
||||
do {
|
||||
filterQueries = targetFilterQueryManagement.findWithAutoAssignDS(query);
|
||||
|
||||
filterQueries.forEach(filterQuery -> {
|
||||
try {
|
||||
filterQuery.getAccessControlContext().ifPresentOrElse(
|
||||
context -> // has stored context - executes it with it
|
||||
contextAware.runInContext(
|
||||
context,
|
||||
() -> consumer.accept(filterQuery)),
|
||||
() -> // has no stored context - executes it in the tenant & user scope
|
||||
contextAware.runAsTenantAsUser(
|
||||
contextAware.getCurrentTenant(),
|
||||
getAutoAssignmentInitiatedBy(filterQuery), () -> {
|
||||
consumer.accept(filterQuery);
|
||||
return null;
|
||||
})
|
||||
);
|
||||
} catch (final RuntimeException ex) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Exception on forEachFilterWithAutoAssignDS execution for filter id {}. Continue with next filter query.",
|
||||
filterQuery.getId(), ex);
|
||||
} else {
|
||||
log.error(
|
||||
"Exception on forEachFilterWithAutoAssignDS execution for filter id {} and error message [{}]. Continue with next filter query.",
|
||||
filterQuery.getId(), ex.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
} while (filterQueries.hasNext() && (query = filterQueries.nextPageable()) != Pageable.unpaged());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs target assignments within a dedicated transaction for a given list of controllerIDs
|
||||
*
|
||||
* @param targetFilterQuery the target filter query
|
||||
* @param controllerIds the controllerIDs
|
||||
* @return count of targets
|
||||
*/
|
||||
private int runTransactionalAssignment(final TargetFilterQuery targetFilterQuery, final List<String> controllerIds) {
|
||||
final String actionMessage = String.format(ACTION_MESSAGE, targetFilterQuery.getName());
|
||||
return DeploymentHelper.runInNewTransaction(transactionManager, "autoAssignDSToTargets", Isolation.READ_COMMITTED.value(), status -> {
|
||||
final List<DeploymentRequest> deploymentRequests = mapToDeploymentRequests(controllerIds, targetFilterQuery);
|
||||
final int count = deploymentRequests.size();
|
||||
if (count > 0) {
|
||||
deploymentManagement.assignDistributionSets(getAutoAssignmentInitiatedBy(targetFilterQuery), deploymentRequests, actionMessage);
|
||||
}
|
||||
return count;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list of {@link DeploymentRequest} for given list of controllerIds and {@link TargetFilterQuery}
|
||||
*
|
||||
* @param controllerIds list of controllerIds
|
||||
* @param filterQuery the query the targets have to match
|
||||
* @return list of deployment request
|
||||
*/
|
||||
private List<DeploymentRequest> mapToDeploymentRequests(final List<String> controllerIds, final TargetFilterQuery filterQuery) {
|
||||
// the action type is set to FORCED per default (when not explicitly specified)
|
||||
final Action.ActionType autoAssignActionType = filterQuery.getAutoAssignActionType() == null
|
||||
? Action.ActionType.FORCED
|
||||
: filterQuery.getAutoAssignActionType();
|
||||
return controllerIds.stream()
|
||||
.map(controllerId -> DeploymentRequest
|
||||
.builder(controllerId, filterQuery.getAutoAssignDistributionSet().getId())
|
||||
.actionType(autoAssignActionType).weight(filterQuery.getAutoAssignWeight().orElse(null))
|
||||
.confirmationRequired(filterQuery.isConfirmationRequired()).build())
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void checkForDevice(final String controllerId, final TargetFilterQuery targetFilterQuery) {
|
||||
log.debug("Auto assign check call for tenant {} and target filter query id {} for device {} started",
|
||||
getContextAware().getCurrentTenant(), targetFilterQuery.getId(), controllerId);
|
||||
log.debug("Auto assign check call for target filter query id {} for device {} started", targetFilterQuery.getId(), controllerId);
|
||||
try {
|
||||
final boolean controllerIdMatches = targetManagement.isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(
|
||||
controllerId, targetFilterQuery.getAutoAssignDistributionSet().getId(),
|
||||
targetFilterQuery.getQuery());
|
||||
|
||||
if (controllerIdMatches) {
|
||||
if (targetManagement.isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(
|
||||
controllerId, targetFilterQuery.getAutoAssignDistributionSet().getId(), targetFilterQuery.getQuery())) {
|
||||
runTransactionalAssignment(targetFilterQuery, Collections.singletonList(controllerId));
|
||||
}
|
||||
|
||||
} catch (final PersistenceException | AbstractServerRtException e) {
|
||||
log.error("Error during auto assign check of target filter query id {}", targetFilterQuery.getId(), e);
|
||||
}
|
||||
log.debug("Auto assign check call for tenant {} and target filter query id {} for device {} finished",
|
||||
getContextAware().getCurrentTenant(), targetFilterQuery.getId(), controllerId);
|
||||
log.debug("Auto assign check call for target filter query id {} for device {} finished", targetFilterQuery.getId(), controllerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 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 java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSetType;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.model.TargetType;
|
||||
import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch;
|
||||
import org.eclipse.hawkbit.repository.test.util.WithUser;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
@TestPropertySource(properties = "hawkbit.acm.access-controller.enabled=true")
|
||||
abstract class AbstractAccessControllerTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
protected SoftwareModuleType smType1;
|
||||
protected SoftwareModuleType smType2;
|
||||
protected SoftwareModule sm1Type1;
|
||||
protected SoftwareModule sm2Type2;
|
||||
protected SoftwareModule sm3Type2;
|
||||
|
||||
protected DistributionSetType dsType1;
|
||||
protected DistributionSetType dsType2;
|
||||
protected DistributionSet ds1Type1;
|
||||
protected DistributionSet ds2Type2;
|
||||
protected DistributionSet ds3Type2;
|
||||
|
||||
protected TargetType targetType1;
|
||||
protected TargetType targetType2;
|
||||
protected Target target1Type1;
|
||||
protected Target target2Type2;
|
||||
protected Target target3Type2;
|
||||
|
||||
@BeforeEach
|
||||
@Override
|
||||
public void beforeAll() throws Exception {
|
||||
super.beforeAll();
|
||||
|
||||
smType1 = testdataFactory.findOrCreateSoftwareModuleType("SmType1");
|
||||
smType2 = testdataFactory.findOrCreateSoftwareModuleType("SmType2");
|
||||
sm1Type1 = softwareModuleManagement.lock(testdataFactory.createSoftwareModule(smType1.getKey()));
|
||||
sm2Type2 = softwareModuleManagement.lock(testdataFactory.createSoftwareModule(smType2.getKey()));
|
||||
sm3Type2 = softwareModuleManagement.lock(testdataFactory.createSoftwareModule(smType2.getKey()));
|
||||
|
||||
dsType1 = testdataFactory.findOrCreateDistributionSetType("DsType1", "DistributionSetType-1", List.of(smType1), List.of());
|
||||
dsType2 = testdataFactory.findOrCreateDistributionSetType("DsType2", "DistributionSetType-2", List.of(smType2), List.of(smType1));
|
||||
ds1Type1 = distributionSetManagement.lock(
|
||||
testdataFactory.createDistributionSet("Ds1Type1", "1.0", dsType1, List.of(sm1Type1)));
|
||||
ds2Type2 = distributionSetManagement.lock(
|
||||
testdataFactory.createDistributionSet("Ds2Type2", "1.0", dsType2, List.of(sm2Type2, sm1Type1)));
|
||||
ds3Type2 = distributionSetManagement.lock(
|
||||
testdataFactory.createDistributionSet("Ds3Type2", "1.0", dsType2, List.of(sm3Type2, sm1Type1)));
|
||||
|
||||
targetType1 = testdataFactory.createTargetType("TargetType1", Set.of(dsType1, dsType2));
|
||||
targetType2 = testdataFactory.createTargetType("TargetType2", Set.of(dsType2));
|
||||
target1Type1 = testdataFactory.createTarget("controller_1", "Controller-1", targetType1);
|
||||
target2Type2 = testdataFactory.createTarget("controller_2", "Controller-2", targetType2);
|
||||
target3Type2 = testdataFactory.createTarget("controller_3", "Controller-3", targetType2);
|
||||
}
|
||||
|
||||
protected static WithUser withAuthorities(final String... authorities) {
|
||||
AuthorityChecker.validateAuthorities(authorities);
|
||||
return SecurityContextSwitch.withUser("user", authorities);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/**
|
||||
* 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.security.SecurityContextSerializer;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
class AcmTestConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
SecurityContextSerializer securityContextSerializer() {
|
||||
return SecurityContextSerializer.JSON_SERIALIZATION;
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
@ContextConfiguration(classes = { AccessControllerConfiguration.class })
|
||||
@TestPropertySource(properties = "hawkbit.acm.access-controller.enabled=true")
|
||||
class ActionAccessControllerTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* 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.eclipse.hawkbit.im.authentication.SpPermission.CREATE_TARGET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.DELETE_TARGET;
|
||||
import static org.eclipse.hawkbit.im.authentication.SpPermission.READ_DISTRIBUTION_SET;
|
||||
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.callAs;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.eclipse.hawkbit.repository.Identifiable;
|
||||
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement;
|
||||
import org.eclipse.hawkbit.repository.TargetFilterQueryManagement.AutoAssignDistributionSetUpdate;
|
||||
import org.eclipse.hawkbit.repository.autoassign.AutoAssignExecutor;
|
||||
import org.eclipse.hawkbit.repository.jpa.autoassign.AutoAssignScheduler;
|
||||
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.integration.support.locks.LockRegistry;
|
||||
|
||||
class AutoAssignTest extends AbstractAccessControllerTest {
|
||||
|
||||
@Autowired
|
||||
AutoAssignExecutor autoAssignExecutor;
|
||||
|
||||
@Autowired
|
||||
LockRegistry lockRegistry;
|
||||
|
||||
@Test
|
||||
void verifyOnlyUpdatableTargetsArePartOfAutoAssignmentByScheduler() throws Exception {
|
||||
// auto assign scheduler apply stored access control context and the context is correctly applied
|
||||
verifyOnlyUpdatableTargetsArePartOfAutoAssignment(
|
||||
() -> new AutoAssignScheduler(systemManagement, systemSecurityContext, autoAssignExecutor, lockRegistry, Optional.empty())
|
||||
.autoAssignScheduler());
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyOnlyUpdatableTargetsArePartOfAutoAssignment() throws Exception {
|
||||
verifyOnlyUpdatableTargetsArePartOfAutoAssignment(autoAssignExecutor::checkAllTargets);
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyOnlyUpdatableTargetsWillGetAssignmentOnSingleCheck() throws Exception {
|
||||
verifyOnlyUpdatableTargetsArePartOfAutoAssignment(() -> {
|
||||
autoAssignExecutor.checkSingleTarget(target1Type1.getControllerId());
|
||||
autoAssignExecutor.checkSingleTarget(target2Type2.getControllerId());
|
||||
autoAssignExecutor.checkSingleTarget(target3Type2.getControllerId());
|
||||
});
|
||||
}
|
||||
|
||||
private void verifyOnlyUpdatableTargetsArePartOfAutoAssignment(final Runnable assigner) throws Exception {
|
||||
final TargetFilterQuery targetFilterQuery = callAs(withAuthorities(
|
||||
CREATE_TARGET,
|
||||
READ_TARGET + "/controllerid==*",
|
||||
UPDATE_TARGET + "/type.id==" + targetType2.getId(), // only updatable (i.e. of targetType2) shall be assigned
|
||||
DELETE_TARGET + "/type.id==" + targetType1.getId(),
|
||||
READ_DISTRIBUTION_SET + "/type.id==" + dsType2.getId()),
|
||||
() -> {
|
||||
final TargetFilterQuery targetFilter = targetFilterQueryManagement
|
||||
.create(TargetFilterQueryManagement.Create.builder().name("testAutoAssignment").query("controllerid==*").build());
|
||||
return targetFilterQueryManagement.updateAutoAssignDS(
|
||||
new AutoAssignDistributionSetUpdate(targetFilter.getId()).ds(ds2Type2.getId()));
|
||||
});
|
||||
|
||||
// do the assignment
|
||||
assigner.run();
|
||||
|
||||
assertThat(targetManagement.findByAssignedDistributionSet(targetFilterQuery.getAutoAssignDistributionSet().getId(), Pageable.unpaged())
|
||||
.map(Identifiable::getId).toList())
|
||||
.as("Only updatable targets should be part of the rollout")
|
||||
// all targets are distribution set type 2 compatible, but since user has UPDATE_TARGET only for targets of type 2
|
||||
// only target2 and target3 shall be assigned
|
||||
.containsExactly(target2Type2.getId(), target3Type2.getId());
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
@@ -26,11 +26,15 @@ import org.eclipse.hawkbit.repository.autoassign.AutoAssignExecutor;
|
||||
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
|
||||
import org.eclipse.hawkbit.repository.model.Rollout;
|
||||
import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
|
||||
import org.eclipse.hawkbit.security.SecurityContextSerializer;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.domain.AuditorAware;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
@@ -43,10 +47,10 @@ import org.springframework.test.context.ContextConfiguration;
|
||||
* Feature: Component Tests - Context runner<br/>
|
||||
* Story: Test Context Runner
|
||||
*/
|
||||
@ContextConfiguration(classes = { AcmTestConfiguration.class })
|
||||
@ContextConfiguration(classes = { ContextAwareTest.TestConfiguration.class })
|
||||
class ContextAwareTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
private static final List<String> AUTHORITIES = SpPermission.getAllAuthorities();
|
||||
private static final Set<String> AUTHORITIES = SpPermission.getAllAuthorities();
|
||||
|
||||
@Autowired
|
||||
AutoAssignExecutor autoAssignExecutor;
|
||||
@@ -169,4 +173,14 @@ class ContextAwareTest extends AbstractJpaIntegrationTest {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
SecurityContextSerializer securityContextSerializer() {
|
||||
return SecurityContextSerializer.JSON_SERIALIZATION;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,6 @@ import org.springframework.test.context.TestPropertySource;
|
||||
* Feature: Component Tests - Access Control<br/>
|
||||
* Story: Test Distribution Set Access Controller
|
||||
*/
|
||||
@ContextConfiguration(classes = { AccessControllerConfiguration.class })
|
||||
@TestPropertySource(properties = "hawkbit.acm.access-controller.enabled=true")
|
||||
class DistributionSetAccessControllerTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
|
||||
@@ -43,14 +43,12 @@ 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;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
/**
|
||||
* Feature: Component Tests - Access Control<br/>
|
||||
* Story: Test Target Access Controller
|
||||
*/
|
||||
@ContextConfiguration(classes = { AccessControllerConfiguration.class, AcmTestConfiguration.class })
|
||||
@TestPropertySource(properties = "hawkbit.acm.access-controller.enabled=true")
|
||||
class TargetAccessControllerTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
|
||||
@@ -30,14 +30,12 @@ import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
|
||||
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;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
/**
|
||||
* Feature: Component Tests - Access Control<br/>
|
||||
* Story: Test Target Type Access Controller
|
||||
*/
|
||||
@ContextConfiguration(classes = { AccessControllerConfiguration.class })
|
||||
@TestPropertySource(properties = "hawkbit.acm.access-controller.enabled=true")
|
||||
class TargetTypeAccessControllerTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
@@ -98,6 +99,7 @@ public class TestdataFactory {
|
||||
public static final String INVISIBLE_SM_MD_VALUE = "invisibleMetdataValue";
|
||||
|
||||
public static final RandomStringUtils RANDOM_STRING_UTILS = RandomStringUtils.secure();
|
||||
public static final AtomicLong COUNTER = new AtomicLong();
|
||||
|
||||
/**
|
||||
* default {@link Target#getControllerId()}.
|
||||
@@ -359,10 +361,8 @@ public class TestdataFactory {
|
||||
/**
|
||||
* Creates {@link DistributionSet} in repository.
|
||||
*
|
||||
* @param prefix for {@link SoftwareModule}s and {@link DistributionSet}s name,
|
||||
* vendor and description.
|
||||
* @param version {@link DistributionSet#getVersion()} and
|
||||
* {@link SoftwareModule#getVersion()} extended by a random number.
|
||||
* @param prefix for {@link SoftwareModule}s and {@link DistributionSet}s name, vendor and description.
|
||||
* @param version {@link DistributionSet#getVersion()} and {@link SoftwareModule#getVersion()} extended by a random number.
|
||||
* @param isRequiredMigrationStep for {@link DistributionSet#isRequiredMigrationStep()}
|
||||
* @param modules for {@link DistributionSet#getModules()}
|
||||
* @return {@link DistributionSet} entity.
|
||||
@@ -395,7 +395,7 @@ public class TestdataFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link DistributionSet}s in repository including three {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} ,
|
||||
* Creates {@link DistributionSet}s in repository including three {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT},
|
||||
* {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION} followed by an iterative number and {@link DistributionSet#isRequiredMigrationStep()}
|
||||
* <code>false</code>.
|
||||
*
|
||||
@@ -427,7 +427,7 @@ public class TestdataFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link DistributionSet}s in repository including three {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} ,
|
||||
* Creates {@link DistributionSet}s in repository including three {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT},
|
||||
* {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION} followed by an iterative count and {@link DistributionSet#isRequiredMigrationStep()}
|
||||
* <code>false</code>.
|
||||
*
|
||||
@@ -568,7 +568,7 @@ public class TestdataFactory {
|
||||
return softwareModuleManagement.create(
|
||||
SoftwareModuleManagement.Create.builder()
|
||||
.type(findOrCreateSoftwareModuleType(typeKey))
|
||||
.name(prefix + typeKey)
|
||||
.name(prefix + typeKey + "_" + COUNTER.incrementAndGet())
|
||||
.version(prefix + DEFAULT_VERSION)
|
||||
.description(randomDescriptionShort())
|
||||
.vendor(DEFAULT_VENDOR)
|
||||
@@ -617,7 +617,7 @@ public class TestdataFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link DistributionSet}s in repository including three {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT} ,
|
||||
* Creates {@link DistributionSet}s in repository including three {@link SoftwareModule}s of types {@link #SM_TYPE_OS}, {@link #SM_TYPE_RT},
|
||||
* {@link #SM_TYPE_APP} with {@link #DEFAULT_VERSION} followed by an iterative number and {@link DistributionSet#isRequiredMigrationStep()}
|
||||
* <code>false</code>.
|
||||
* <p/>
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
*/
|
||||
package org.eclipse.hawkbit.im.authentication;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -124,8 +124,11 @@ public final class SpPermission {
|
||||
TENANT_CONFIGURATION + IMPLY + READ_GATEWAY_SECURITY_TOKEN + LINE_BREAK;
|
||||
|
||||
// @formatter:on
|
||||
private static final SingletonSupplier<List<String>> ALL_AUTHORITIES = SingletonSupplier.of(() -> {
|
||||
final List<String> allPermissions = new ArrayList<>();
|
||||
private static final SingletonSupplier<Set<String>> ALL_AUTHORITIES = SingletonSupplier.of(() -> getAuthorities(false));
|
||||
private static final SingletonSupplier<Set<String>> ALL_TENANT_AUTHORITIES = SingletonSupplier.of(() -> getAuthorities(true));
|
||||
|
||||
private static Set<String> getAuthorities(final boolean tenant) {
|
||||
final Set<String> allPermissions = new HashSet<>();
|
||||
|
||||
// groups with access, canonical
|
||||
for (final String group : new String[] {
|
||||
@@ -150,18 +153,19 @@ public final class SpPermission {
|
||||
}
|
||||
allPermissions.add(TENANT_CONFIGURATION);
|
||||
|
||||
if (!tenant) {
|
||||
// system permission, (!) take care with
|
||||
allPermissions.add(SYSTEM_ADMIN);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(allPermissions);
|
||||
});
|
||||
return Collections.unmodifiableSet(allPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all permission.
|
||||
*
|
||||
* @return all permissions
|
||||
*/
|
||||
public static List<String> getAllAuthorities() {
|
||||
public static Set<String> getAllAuthorities() {
|
||||
return ALL_AUTHORITIES.get();
|
||||
}
|
||||
|
||||
public static Set<String> getAllTenantAuthorities() {
|
||||
return ALL_TENANT_AUTHORITIES.get();
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,9 @@ package org.eclipse.hawkbit.security;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.eclipse.hawkbit.security.SecurityContextSerializer.JSON_SERIALIZATION;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAwareAuthenticationDetails;
|
||||
@@ -27,7 +28,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
class SecurityContextSerializerTest {
|
||||
|
||||
private static final List<String> AUTHORITIES = SpPermission.getAllAuthorities();
|
||||
private static final Set<String> AUTHORITIES = SpPermission.getAllAuthorities();
|
||||
|
||||
@Test
|
||||
void testJsonSerialization() {
|
||||
@@ -42,7 +43,7 @@ class SecurityContextSerializerTest {
|
||||
final SecurityContext deserialized = JSON_SERIALIZATION.deserialize(serialized);
|
||||
final Authentication authentication = deserialized.getAuthentication();
|
||||
assertThat(SpringSecurityAuditorAware.resolveAuditor(authentication)).hasToString("user");
|
||||
assertThat(authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList()).isEqualTo(AUTHORITIES);
|
||||
assertThat(authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet())).isEqualTo(AUTHORITIES);
|
||||
assertThat(authentication.isAuthenticated()).isTrue();
|
||||
assertThat(authentication.getDetails()).isEqualTo(details);
|
||||
}
|
||||
@@ -75,7 +76,7 @@ class SecurityContextSerializerTest {
|
||||
final SecurityContext deserialized = JSON_SERIALIZATION.deserialize(serialized);
|
||||
final Authentication authentication = deserialized.getAuthentication();
|
||||
assertThat(SpringSecurityAuditorAware.resolveAuditor(authentication)).hasToString("user");
|
||||
assertThat(authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList()).isEqualTo(AUTHORITIES);
|
||||
assertThat(authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet())).isEqualTo(AUTHORITIES);
|
||||
assertThat(authentication.isAuthenticated()).isTrue();
|
||||
assertThat(authentication.getDetails()).isEqualTo(details);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user