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:
Avgustin Marinov
2025-10-07 15:26:04 +03:00
committed by GitHub
parent 6907931eb6
commit cc36ca8801
18 changed files with 508 additions and 297 deletions

View File

@@ -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).

View File

@@ -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
View 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 }}

View File

@@ -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) {

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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());
}
}

View File

@@ -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;
}
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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/>

View File

@@ -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();
}
}

View File

@@ -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);
}