Skip to content

Commit

Permalink
Fix #351: Service for automatic cleanup of activations with failed on…
Browse files Browse the repository at this point in the history
…boarding
  • Loading branch information
banterCZ committed Sep 29, 2022
1 parent 79b5ee2 commit b56582a
Show file tree
Hide file tree
Showing 11 changed files with 266 additions and 4 deletions.
1 change: 1 addition & 0 deletions docs/sql/mysql/onboarding/create-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ CREATE TABLE es_onboarding_process (
user_id VARCHAR(256),
activation_id VARCHAR(36),
status VARCHAR(32) NOT NULL,
activation_removed TINYINT DEFAULT 0,
error_detail VARCHAR(256),
error_origin VARCHAR(256),
error_score INTEGER NOT NULL DEFAULT 0,
Expand Down
1 change: 1 addition & 0 deletions docs/sql/oracle/onboarding/create-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ CREATE TABLE ES_ONBOARDING_PROCESS (
USER_ID VARCHAR2(256 CHAR),
ACTIVATION_ID VARCHAR2(36 CHAR),
STATUS VARCHAR2(32 CHAR) NOT NULL,
ACTIVATION_REMOVED NUMBER(1) DEFAULT 0,
ERROR_DETAIL VARCHAR2(256 CHAR),
ERROR_ORIGIN VARCHAR2(256 CHAR),
ERROR_SCORE INTEGER DEFAULT 0 NOT NULL,
Expand Down
1 change: 1 addition & 0 deletions docs/sql/postgresql/onboarding/create-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ CREATE TABLE es_onboarding_process (
user_id VARCHAR(256),
activation_id VARCHAR(36),
status VARCHAR(32) NOT NULL,
activation_removed BOOLEAN DEFAULT FALSE,
error_detail VARCHAR(256),
error_origin VARCHAR(256),
error_score INTEGER NOT NULL DEFAULT 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import com.wultra.app.enrollmentserver.model.enumeration.ErrorOrigin;
import com.wultra.app.enrollmentserver.model.enumeration.OnboardingStatus;
import com.wultra.app.onboardingserver.common.database.entity.OnboardingProcessEntity;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
Expand Down Expand Up @@ -96,4 +98,27 @@ public interface OnboardingProcessRepository extends CrudRepository<OnboardingPr
"AND p.status <> com.wultra.app.enrollmentserver.model.enumeration.OnboardingStatus.FAILED " +
"AND p.timestampCreated < :dateCreatedBefore")
List<String> findExpiredProcessIdsByCreatedDate(Date dateCreatedBefore);

/**
* Return onboarding processes to remove activation.
*
* @param pageable pagination information
* @return onboarding processes
* @see #findProcessesToRemoveActivation(int)
*/
@Query("SELECT p FROM OnboardingProcessEntity p " +
"WHERE p.status = com.wultra.app.enrollmentserver.model.enumeration.OnboardingStatus.FAILED " +
"AND p.activationId is not null " +
"AND p.activationRemoved = false")
List<OnboardingProcessEntity> findProcessesToRemoveActivation(Pageable pageable);

/**
* Return onboarding processes to remove activation.
*
* @param maxCount max count of results
* @return onboarding processes
*/
default List<OnboardingProcessEntity> findProcessesToRemoveActivation(int maxCount) {
return findProcessesToRemoveActivation(PageRequest.of(0, maxCount));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ public class OnboardingProcessEntity implements Serializable {
@Column(name = "status", nullable = false)
private OnboardingStatus status;

/**
* When the status is {@link OnboardingStatus#FAILED}, the activation specified be {@link #activationId} should be removed at PowerAuth server.
* This flag indicates that the task has been done.
*/
@Column(name = "activation_removed")
private boolean activationRemoved;

@Column(name = "error_detail")
private String errorDetail;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException;
import com.wultra.security.powerauth.client.PowerAuthClient;
import com.wultra.security.powerauth.client.model.error.PowerAuthClientException;
import com.wultra.security.powerauth.client.v3.ActivationStatus;
import com.wultra.security.powerauth.client.v3.GetActivationStatusRequest;
import com.wultra.security.powerauth.client.v3.RemoveActivationRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -62,4 +64,22 @@ public void removeActivation(final String activationId) throws RemoteCommunicati
throw new RemoteCommunicationException("Communication with PowerAuth server failed: " + e.getMessage(), e);
}
}

/**
* Return activation status.
*
* @param activationId Activation ID.
* @return activation status
* @throws RemoteCommunicationException Thrown when communication with PowerAuth server fails.
*/
public ActivationStatus fetchActivationStatus(final String activationId) throws RemoteCommunicationException {
final GetActivationStatusRequest request = new GetActivationStatusRequest();
request.setActivationId(activationId);

try {
return powerAuthClient.getActivationStatus(request).getActivationStatus();
} catch (PowerAuthClientException e) {
throw new RemoteCommunicationException("Communication with PowerAuth server failed: " + e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,15 +227,16 @@ public Response performCleanup(OnboardingCleanupRequest request) throws Onboardi
otpService.cancelOtp(process, OtpType.ACTIVATION);
otpService.cancelOtp(process, OtpType.USER_VERIFICATION);

removeActivation(process);

process.setStatus(OnboardingStatus.FAILED);
process.setErrorDetail(OnboardingProcessEntity.ERROR_PROCESS_CANCELED);
process.setErrorOrigin(ErrorOrigin.USER_REQUEST);
process.setTimestampLastUpdated(new Date());
process.setTimestampFailed(new Date());
process.setActivationRemoved(true);
onboardingProcessRepository.save(process);

removeActivation(process);

return new Response();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* PowerAuth Enrollment Server
* Copyright (C) 2022 Wultra s.r.o.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.wultra.app.onboardingserver.task.cleaning;

import com.wultra.app.onboardingserver.common.database.OnboardingProcessRepository;
import com.wultra.app.onboardingserver.common.database.entity.OnboardingProcessEntity;
import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException;
import com.wultra.app.onboardingserver.impl.service.ActivationService;
import com.wultra.security.powerauth.client.v3.ActivationStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
* Service to cleaning activations.
*
* @author Lubos Racansky, lubos.racansky@wultra.com
*/
@Service
@Slf4j
class ActivationCleaningService {

private static final int BATCH_SIZE = 100;

private final OnboardingProcessRepository onboardingProcessRepository;

private final ActivationService activationService;

@Autowired
public ActivationCleaningService(
final OnboardingProcessRepository onboardingProcessRepository,
final ActivationService activationService) {

this.onboardingProcessRepository = onboardingProcessRepository;
this.activationService = activationService;
}

/**
* Cleanup activations of failed onboarding processes.
*/
public void cleanupActivations() {
final List<OnboardingProcessEntity> processes = onboardingProcessRepository.findProcessesToRemoveActivation(BATCH_SIZE);
if (processes.isEmpty()) {
logger.debug("No onboarding processes to remove activation");
return;
}

processes.forEach(this::cleanupActivation);
}

private void cleanupActivation(final OnboardingProcessEntity process) {
final String activationId = process.getActivationId();
logger.info("Removing activation ID: {} of process ID: {}", activationId, process.getId());

try {
removeActivation(activationId);
process.setActivationRemoved(true);
onboardingProcessRepository.save(process);
} catch (RemoteCommunicationException e) {
logger.error("Unable to remove activation ID: {}", activationId, e);
}
}

private void removeActivation(String activationId) throws RemoteCommunicationException {
final ActivationStatus activationStatus = activationService.fetchActivationStatus(activationId);
if (activationStatus == ActivationStatus.REMOVED) {
logger.debug("Activation ID: {} has been already removed", activationId);
return;
}

activationService.removeActivation(activationId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import org.springframework.stereotype.Component;

/**
* Task to clean expired processes, identity verifications, documents and OTPs.
* Task to clean expired processes, identity verifications, documents, activations and OTPs.
*
* @author Lubos Racansky, lubos.racansky@wultra.com
*/
Expand All @@ -35,9 +35,12 @@ public class CleaningTask {

private final CleaningService cleaningService;

private final ActivationCleaningService activationCleaningService;

@Autowired
public CleaningTask(final CleaningService cleaningService) {
public CleaningTask(final CleaningService cleaningService, final ActivationCleaningService activationCleaningService) {
this.cleaningService = cleaningService;
this.activationCleaningService = activationCleaningService;
}

/**
Expand Down Expand Up @@ -110,4 +113,15 @@ public void terminateExpiredIdentityVerifications() {
logger.debug("terminateExpiredIdentityVerifications");
cleaningService.terminateExpiredIdentityVerifications();
}

/**
* Cleanup activations of failed onboarding processes.
*/
@Scheduled(fixedDelayString = "PT60S", initialDelayString = "PT60S")
@SchedulerLock(name = "cleanupActivations", lockAtLeastFor = "1s", lockAtMostFor = "5m")
public void cleanupActivations() {
LockAssert.assertLocked();
logger.debug("cleanupActivations");
activationCleaningService.cleanupActivations();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* PowerAuth Enrollment Server
* Copyright (C) 2022 Wultra s.r.o.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.wultra.app.onboardingserver.task.cleaning;

import com.wultra.app.onboardingserver.EnrollmentServerTestApplication;
import com.wultra.app.onboardingserver.common.database.entity.OnboardingProcessEntity;
import com.wultra.app.onboardingserver.common.errorhandling.RemoteCommunicationException;
import com.wultra.app.onboardingserver.impl.service.ActivationService;
import com.wultra.security.powerauth.client.v3.ActivationStatus;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;

import static org.mockito.Mockito.*;
import static org.springframework.test.util.AssertionErrors.assertFalse;
import static org.springframework.test.util.AssertionErrors.assertTrue;

/**
* Test for {@link ActivationCleaningService}.
*
* @author Lubos Racansky, lubos.racansky@wultra.com
*/
@SpringBootTest(classes = EnrollmentServerTestApplication.class)
@ActiveProfiles("mock")
@Transactional
@Sql
class ActivationCleaningServiceTest {

@Autowired
private ActivationCleaningService tested;

@MockBean
private ActivationService activationService;

@Autowired
private EntityManager entityManager;

@Test
void testSuccessful() throws Exception {
when(activationService.fetchActivationStatus("a2"))
.thenReturn(ActivationStatus.ACTIVE);

tested.cleanupActivations();

final OnboardingProcessEntity process = fetchOnboardingProcess("22222222-df91-4053-bb3d-3970979baf5d");
assertTrue("activation should be marked as removed", process.isActivationRemoved());
verify(activationService).removeActivation("a2");
}

@Test
void testAlreadyDeleted() throws Exception {
when(activationService.fetchActivationStatus("a2"))
.thenReturn(ActivationStatus.REMOVED);

tested.cleanupActivations();

final OnboardingProcessEntity process = fetchOnboardingProcess("22222222-df91-4053-bb3d-3970979baf5d");
assertTrue("activation should be marked as removed", process.isActivationRemoved());
verify(activationService, never()).removeActivation("a2");
}

@Test
void testCommunicationException() throws Exception {
when(activationService.fetchActivationStatus("a2"))
.thenThrow(new RemoteCommunicationException("test exception"));

tested.cleanupActivations();

final OnboardingProcessEntity process = fetchOnboardingProcess("22222222-df91-4053-bb3d-3970979baf5d");
assertFalse("activation should not be marked as removed", process.isActivationRemoved());
verify(activationService, never()).removeActivation("a2");
}

private OnboardingProcessEntity fetchOnboardingProcess(final String id) {
return entityManager.find(OnboardingProcessEntity.class, id);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
INSERT INTO es_onboarding_process(id, identification_data, status, activation_id, activation_removed, error_score, timestamp_created) VALUES
('11111111-df91-4053-bb3d-3970979baf5d', '{}', 'ACTIVATION_IN_PROGRESS', 'a1', false, 0, now()),
('22222222-df91-4053-bb3d-3970979baf5d', '{}', 'FAILED', 'a2', false, 0, now()),
('33333333-df91-4053-bb3d-3970979baf5d', '{}', 'FAILED', 'a3', true, 0, now());

0 comments on commit b56582a

Please sign in to comment.