diff --git a/.github/workflows/first-interaction.yaml b/.github/workflows/first-interaction.yaml index 11f31c531..2adda1e1d 100644 --- a/.github/workflows/first-interaction.yaml +++ b/.github/workflows/first-interaction.yaml @@ -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). \ No newline at end of file diff --git a/.github/workflows/reusable_workflow_verify.yaml b/.github/workflows/reusable_workflow_verify.yaml index 44e5fc143..44e01e58f 100644 --- a/.github/workflows/reusable_workflow_verify.yaml +++ b/.github/workflows/reusable_workflow_verify.yaml @@ -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 \ No newline at end of file diff --git a/.github/workflows/style_check.yaml b/.github/workflows/style_check.yaml new file mode 100644 index 000000000..9b90a4f4d --- /dev/null +++ b/.github/workflows/style_check.yaml @@ -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 }} diff --git a/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/EntityMatcher.java b/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/EntityMatcher.java index ca38b4ec4..369bda236 100644 --- a/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/EntityMatcher.java +++ b/hawkbit-repository/hawkbit-repository-jpa-ql/src/main/java/org/eclipse/hawkbit/repository/jpa/ql/EntityMatcher.java @@ -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 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 Getter getGetter(final Class 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) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/acm/AuthorityChecker.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/acm/AuthorityChecker.java new file mode 100644 index 000000000..507620cba --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/acm/AuthorityChecker.java @@ -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 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); + } + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AbstractAutoAssignExecutor.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AbstractAutoAssignExecutor.java deleted file mode 100644 index d17dac6c3..000000000 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AbstractAutoAssignExecutor.java +++ /dev/null @@ -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 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 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 consumer) { - Slice 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 controllerIds) { - final String actionMessage = String.format(ACTION_MESSAGE, targetFilterQuery.getName()); - - return DeploymentHelper.runInNewTransaction(getTransactionManager(), "autoAssignDSToTargets", - Isolation.READ_COMMITTED.value(), status -> { - - final List 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 mapToDeploymentRequests(final List 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(); - } -} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java index b54e08519..3d612d931 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/autoassign/AutoAssignChecker.java @@ -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 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 targetFilterQueryManagement; + private final TargetManagement targetManagement; + private final DeploymentManagement deploymentManagement; + private final PlatformTransactionManager transactionManager; + private final ContextAware contextAware; + public AutoAssignChecker( final TargetFilterQueryManagement targetFilterQueryManagement, final TargetManagement 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 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 consumer) { + Slice 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 controllerIds) { + final String actionMessage = String.format(ACTION_MESSAGE, targetFilterQuery.getName()); + return DeploymentHelper.runInNewTransaction(transactionManager, "autoAssignDSToTargets", Isolation.READ_COMMITTED.value(), status -> { + final List 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 mapToDeploymentRequests(final List 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); } -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/AbstractAccessControllerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/AbstractAccessControllerTest.java new file mode 100644 index 000000000..2638b0aea --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/AbstractAccessControllerTest.java @@ -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); + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/AcmTestConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/AcmTestConfiguration.java deleted file mode 100644 index f52fa3633..000000000 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/AcmTestConfiguration.java +++ /dev/null @@ -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; - } -} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ActionAccessControllerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ActionAccessControllerTest.java index 161a28905..8b12ee27d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ActionAccessControllerTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ActionAccessControllerTest.java @@ -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 { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/AutoAssignTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/AutoAssignTest.java new file mode 100644 index 000000000..af4fa96a4 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/AutoAssignTest.java @@ -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()); + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ContextAwareTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ContextAwareTest.java index 1fb410741..f886a9bd4 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ContextAwareTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/ContextAwareTest.java @@ -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
* Story: Test Context Runner */ -@ContextConfiguration(classes = { AcmTestConfiguration.class }) +@ContextConfiguration(classes = { ContextAwareTest.TestConfiguration.class }) class ContextAwareTest extends AbstractJpaIntegrationTest { - private static final List AUTHORITIES = SpPermission.getAllAuthorities(); + private static final Set 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; + } + } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/DistributionSetAccessControllerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/DistributionSetAccessControllerTest.java index 30560b965..f4a1d776c 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/DistributionSetAccessControllerTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/DistributionSetAccessControllerTest.java @@ -47,7 +47,6 @@ import org.springframework.test.context.TestPropertySource; * Feature: Component Tests - Access Control
* Story: Test Distribution Set Access Controller */ -@ContextConfiguration(classes = { AccessControllerConfiguration.class }) @TestPropertySource(properties = "hawkbit.acm.access-controller.enabled=true") class DistributionSetAccessControllerTest extends AbstractJpaIntegrationTest { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetAccessControllerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetAccessControllerTest.java index 5f6d34793..808183388 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetAccessControllerTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetAccessControllerTest.java @@ -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
* Story: Test Target Access Controller */ -@ContextConfiguration(classes = { AccessControllerConfiguration.class, AcmTestConfiguration.class }) @TestPropertySource(properties = "hawkbit.acm.access-controller.enabled=true") class TargetAccessControllerTest extends AbstractJpaIntegrationTest { @@ -185,7 +183,7 @@ class TargetAccessControllerTest extends AbstractJpaIntegrationTest { */ @Test void verifyTargetAssignment() { - final DistributionSet ds = testdataFactory.createDistributionSet("myDs"); + final DistributionSet ds = testdataFactory.createDistributionSet("myDs"); distributionSetManagement.lock(ds); final Target permittedTarget = targetManagement diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetTypeAccessControllerTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetTypeAccessControllerTest.java index 7a44cd586..e5282f43b 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetTypeAccessControllerTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/acm/TargetTypeAccessControllerTest.java @@ -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
* Story: Test Target Type Access Controller */ -@ContextConfiguration(classes = { AccessControllerConfiguration.class }) @TestPropertySource(properties = "hawkbit.acm.access-controller.enabled=true") class TargetTypeAccessControllerTest extends AbstractJpaIntegrationTest { diff --git a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java index 16ae74fd0..bb973b0bb 100644 --- a/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java +++ b/hawkbit-repository/hawkbit-repository-test/src/main/java/org/eclipse/hawkbit/repository/test/util/TestdataFactory.java @@ -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()} * false. * @@ -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()} * false. * @@ -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()} * false. *

@@ -640,7 +640,7 @@ public class TestdataFactory { /** * @return {@link DistributionSetType} with key {@link #DS_TYPE_DEFAULT} and {@link SoftwareModuleType}s {@link #SM_TYPE_OS}, - * {@link #SM_TYPE_RT} , {@link #SM_TYPE_APP}. + * {@link #SM_TYPE_RT} , {@link #SM_TYPE_APP}. */ public DistributionSetType findOrCreateDefaultTestDsType() { final List swt = new ArrayList<>(); diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java index 311c6ec72..171d5459f 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/im/authentication/SpPermission.java @@ -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> ALL_AUTHORITIES = SingletonSupplier.of(() -> { - final List allPermissions = new ArrayList<>(); + private static final SingletonSupplier> ALL_AUTHORITIES = SingletonSupplier.of(() -> getAuthorities(false)); + private static final SingletonSupplier> ALL_TENANT_AUTHORITIES = SingletonSupplier.of(() -> getAuthorities(true)); + + private static Set getAuthorities(final boolean tenant) { + final Set 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); - // system permission, (!) take care with - allPermissions.add(SYSTEM_ADMIN); + 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 getAllAuthorities() { + public static Set getAllAuthorities() { return ALL_AUTHORITIES.get(); } + + public static Set getAllTenantAuthorities() { + return ALL_TENANT_AUTHORITIES.get(); + } } \ No newline at end of file diff --git a/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/security/SecurityContextSerializerTest.java b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/security/SecurityContextSerializerTest.java index 75c66a900..d9d250582 100644 --- a/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/security/SecurityContextSerializerTest.java +++ b/hawkbit-security-core/src/test/java/org/eclipse/hawkbit/security/SecurityContextSerializerTest.java @@ -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 AUTHORITIES = SpPermission.getAllAuthorities(); + private static final Set 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); }