diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java index 1a432c7a1..712ce8637 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/ControllerManagement.java @@ -429,4 +429,26 @@ public interface ControllerManagement { */ @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) Action cancelAction(long actionId); + + /** + * Updates given {@link Action} with its external id. + * + * @param actionId + * to be updated + * @param externalRef + * of the action + */ + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + void updateActionExternalRef(long actionId, @NotEmpty String externalRef); + + /** + * Retrieves list of {@link Action}s which matches the provided + * externalRefs. + * + * @param externalRefs + * for which the actions need to be fetched. + * @return list of {@link Action}s matching the externalRefs. + */ + @PreAuthorize(SpringEvalExpressions.IS_CONTROLLER) + List getActiveActionsByExternalRef(@NotNull List externalRefs); } diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java index 1debf906e..30c2ef475 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/model/Action.java @@ -12,6 +12,8 @@ import java.time.ZonedDateTime; import java.util.Optional; import java.util.concurrent.TimeUnit; +import javax.validation.constraints.NotEmpty; + /** * Update operations to be executed by the target. */ @@ -32,6 +34,11 @@ public interface Action extends TenantAwareBaseEntity { */ int MAINTENANCE_WINDOW_TIMEZONE_LENGTH = 8; + /** + * Maximum length of external reference. + */ + int EXTERNAL_REF_MAX_LENGTH = 512; + /** * @return the distributionSet */ @@ -98,6 +105,16 @@ public interface Action extends TenantAwareBaseEntity { */ String getMaintenanceWindowTimeZone(); + /** + * @param externalRef associated with this action + */ + void setExternalRef(@NotEmpty String externalRef); + + /** + * @return externalRef of the action + */ + String getExternalRef(); + /** * checks if the {@link #getForcedTime()} is hit by the given * {@code hitTimeMillis}, by means if the given milliseconds are greater diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java index e9dfff7d3..25df72e03 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/ActionRepository.java @@ -240,6 +240,18 @@ public interface ActionRepository extends BaseEntityRepository, @Param("targetsIds") List targetIds, @Param("active") boolean active, @Param("currentStatus") Action.Status currentStatus); + /** + * Retrieves all {@link Action}s that matches the queried externalRefs. + * + * @param externalRefs + * for which the actions need to be found + * @param active + * flag to indicate active/inactive actions + * @return list of actions + */ + List findByExternalRefInAndActive(@Param("externalRefs") List externalRefs, + @Param("active") boolean active); + /** * Switches the status of actions from one specific status into another for * given actions IDs, active flag and current status @@ -504,4 +516,16 @@ public interface ActionRepository extends BaseEntityRepository, @Query("DELETE FROM JpaAction a WHERE a.id IN ?1") void deleteByIdIn(Collection actionIDs); + /** + * Updates the externalRef of an action by its actionId. + * + * @param actionId + * for which the externalRef is being updated. + * @param externalRef + * value of the external reference for the given action id. + */ + @Modifying + @Transactional + @Query("UPDATE JpaAction a SET a.externalRef = :externalRef WHERE a.id = :actionId") + void updateExternalRef(@Param("actionId") Long actionId, @Param("externalRef") String externalRef); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java index e298dea2e..2fc88e9f6 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaControllerManagement.java @@ -36,6 +36,8 @@ import javax.persistence.Query; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Root; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; import org.eclipse.hawkbit.repository.ControllerManagement; import org.eclipse.hawkbit.repository.EntityFactory; @@ -372,6 +374,11 @@ public class JpaControllerManagement implements ControllerManagement { return actionRepository.getById(actionId); } + @Override + public List getActiveActionsByExternalRef(@NotNull final List externalRefs) { + return actionRepository.findByExternalRefInAndActive(externalRefs, true); + } + @Override @Transactional(isolation = Isolation.READ_COMMITTED) @Retryable(include = ConcurrencyFailureException.class, exclude = EntityAlreadyExistsException.class, maxAttempts = Constants.TX_RT_MAX, backoff = @Backoff(delay = Constants.TX_RT_DELAY)) @@ -1042,6 +1049,11 @@ public class JpaControllerManagement implements ControllerManagement { } } + @Override + public void updateActionExternalRef(final long actionId, @NotEmpty final String externalRef) { + actionRepository.updateExternalRef(actionId, externalRef); + } + private void cancelAssignDistributionSetEvent(final JpaTarget target, final Long actionId) { afterCommit.afterCommit( () -> eventPublisher.publishEvent(new CancelTargetAssignmentEvent(target, actionId, bus.getId()))); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java index 6c9752513..243d07afd 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaAction.java @@ -126,6 +126,9 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio @Column(name = "maintenance_time_zone", updatable = false, length = Action.MAINTENANCE_WINDOW_TIMEZONE_LENGTH) private String maintenanceWindowTimeZone; + + @Column(name = "external_ref", length = Action.EXTERNAL_REF_MAX_LENGTH) + private String externalRef; @Override public DistributionSet getDistributionSet() { @@ -332,4 +335,14 @@ public class JpaAction extends AbstractJpaTenantAwareBaseEntity implements Actio } } } + + @Override + public void setExternalRef(String externalRef) { + this.externalRef = externalRef; + } + + @Override + public String getExternalRef() { + return externalRef; + } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_13__add_action_external_id___DB2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_13__add_action_external_id___DB2.sql new file mode 100644 index 000000000..8dbda293f --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/DB2/V1_12_13__add_action_external_id___DB2.sql @@ -0,0 +1,4 @@ +ALTER TABLE sp_action ADD COLUMN external_ref VARCHAR(512); +CREATE INDEX sp_idx_action_external_ref ON sp_action (external_ref); + + \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_13__add_action_external_id___H2.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_13__add_action_external_id___H2.sql new file mode 100644 index 000000000..8dbda293f --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/H2/V1_12_13__add_action_external_id___H2.sql @@ -0,0 +1,4 @@ +ALTER TABLE sp_action ADD COLUMN external_ref VARCHAR(512); +CREATE INDEX sp_idx_action_external_ref ON sp_action (external_ref); + + \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_13__add_action_external_id___MYSQL.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_13__add_action_external_id___MYSQL.sql new file mode 100644 index 000000000..8dbda293f --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/MYSQL/V1_12_13__add_action_external_id___MYSQL.sql @@ -0,0 +1,4 @@ +ALTER TABLE sp_action ADD COLUMN external_ref VARCHAR(512); +CREATE INDEX sp_idx_action_external_ref ON sp_action (external_ref); + + \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_13__add_action_external_id___SQL_SERVER.sql b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_13__add_action_external_id___SQL_SERVER.sql new file mode 100644 index 000000000..b5ca92aec --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/resources/db/migration/SQL_SERVER/V1_12_13__add_action_external_id___SQL_SERVER.sql @@ -0,0 +1,4 @@ +ALTER TABLE sp_action ADD external_ref VARCHAR(512); +CREATE INDEX sp_idx_action_external_ref ON sp_action (external_ref); + + \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java index 940c0721f..1cc088239 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/ControllerManagementTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -54,6 +55,7 @@ import org.eclipse.hawkbit.repository.exception.InvalidTargetAttributeException; import org.eclipse.hawkbit.repository.exception.QuotaExceededException; import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.Action; +import org.eclipse.hawkbit.repository.model.Action.ActionType; import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.ActionStatus; import org.eclipse.hawkbit.repository.model.Artifact; @@ -1315,6 +1317,44 @@ public class ControllerManagementTest extends AbstractJpaIntegrationTest { } } + @Test + @Description("Verify that the attaching externalRef to an action is propery stored") + public void updatedExternalRefOnActionIsReallyUpdated() { + final List allExternalRef = new ArrayList<>(); + final List allActionId = new ArrayList<>(); + final int numberOfActions = 3; + final DistributionSet knownDistributionSet = testdataFactory.createDistributionSet(); + for (int i = 0; i < numberOfActions; i++) { + final String knownControllerId = "controllerId" + i; + final String knownExternalref = "externalRefId" + i; + + testdataFactory.createTarget(knownControllerId); + final DistributionSetAssignmentResult assignmentResult = deploymentManagement.assignDistributionSet( + knownDistributionSet.getId(), ActionType.FORCED, 0, Collections.singleton(knownControllerId)); + final Long actionId = assignmentResult.getActionIds().get(0); + controllerManagement.updateActionExternalRef(actionId, knownExternalref); + + allExternalRef.add(knownExternalref); + allActionId.add(actionId); + } + + final List foundAction = controllerManagement.getActiveActionsByExternalRef(allExternalRef); + assertThat(foundAction).isNotNull(); + for (int i = 0; i < numberOfActions; i++) { + assertThat(foundAction.get(i).getId()).isEqualTo(allActionId.get(i)); + } + } + + @Test + @Description("Verify that a null externalRef cannot be assigned to an action") + public void externalRefCannotBeNull() { + try { + controllerManagement.updateActionExternalRef(1L, null); + fail("No ConstraintViolationException thrown when a null externalRef was set on an action"); + } catch (final ConstraintViolationException e) { + } + } + @Test @Description("Verifies that a target can report FINISHED/ERROR updates for DOWNLOAD_ONLY assignments regardless of " + "repositoryProperties.rejectActionStatusForClosedAction value.") diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java index d86d5a134..347dcbce4 100644 --- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java +++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/SystemSecurityContext.java @@ -11,12 +11,18 @@ package org.eclipse.hawkbit.security; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.UUID; import java.util.concurrent.Callable; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +import org.eclipse.hawkbit.im.authentication.TenantAwareAuthenticationDetails; import org.eclipse.hawkbit.im.authentication.SpPermission.SpringEvalExpressions; import org.eclipse.hawkbit.tenancy.TenantAware; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -111,6 +117,42 @@ public class SystemSecurityContext { } } + /** + * Runs a given {@link Callable} within a system security context, which has + * the provided {@link GrantedAuthority}s to successfully run the + * {@link Callable}. + * + * The security context will be switched to the a new + * {@link SecurityContext} and back after the callable is called. + * + * @param tenant + * under which the {@link Callable#call()} must be executed. + * @param callable + * to call within the security context + * @return the return value of the {@link Callable#call()} method. + */ + // The callable API throws a Exception and not a specific one + @SuppressWarnings({ "squid:S2221", "squid:S00112" }) + public T runAsControllerAsTenant(@NotEmpty final String tenant, @NotNull final Callable callable) { + final SecurityContext oldContext = SecurityContextHolder.getContext(); + List authorities = Collections + .singletonList(new SimpleGrantedAuthority(SpringEvalExpressions.CONTROLLER_ROLE_ANONYMOUS)); + try { + return tenantAware.runAsTenant(tenant, () -> { + try { + setCustomSecurityContext(tenant, oldContext.getAuthentication().getPrincipal(), authorities); + return callable.call(); + + } catch (final Exception e) { + throw new RuntimeException(e); + } + }); + + } finally { + SecurityContextHolder.setContext(oldContext); + } + } + /** * @return {@code true} if the current running code is running as system * code block. @@ -119,6 +161,16 @@ public class SystemSecurityContext { return SecurityContextHolder.getContext().getAuthentication() instanceof SystemCodeAuthentication; } + private void setCustomSecurityContext(final String tenantId, final Object principal, + final Collection authorities) { + final AnonymousAuthenticationToken authenticationToken = new AnonymousAuthenticationToken( + UUID.randomUUID().toString(), principal, authorities); + authenticationToken.setDetails(new TenantAwareAuthenticationDetails(tenantId, true)); + final SecurityContextImpl securityContextImpl = new SecurityContextImpl(); + securityContextImpl.setAuthentication(authenticationToken); + SecurityContextHolder.setContext(securityContextImpl); + } + private static void setSystemContext(final SecurityContext oldContext) { final Authentication oldAuthentication = oldContext.getAuthentication(); final SecurityContextImpl securityContextImpl = new SecurityContextImpl();