diff --git a/code/gms-backend/pom.xml b/code/gms-backend/pom.xml index f8c8f8b7..da7d6d07 100644 --- a/code/gms-backend/pom.xml +++ b/code/gms-backend/pom.xml @@ -180,7 +180,7 @@ org.springframework.ldap spring-ldap-core - 3.2.4 + 3.2.8 com.unboundid diff --git a/code/gms-backend/src/main/java/io/github/gms/common/abstraction/AbstractJob.java b/code/gms-backend/src/main/java/io/github/gms/common/abstraction/AbstractJob.java index 1d394aac..cf714822 100644 --- a/code/gms-backend/src/main/java/io/github/gms/common/abstraction/AbstractJob.java +++ b/code/gms-backend/src/main/java/io/github/gms/common/abstraction/AbstractJob.java @@ -23,6 +23,7 @@ import static io.github.gms.common.enums.JobStatus.COMPLETED; import static io.github.gms.common.enums.JobStatus.FAILED; +import static io.github.gms.common.util.Constants.TRUE; /** * @author Peter Szrnka @@ -73,7 +74,7 @@ protected void execute(BusinessLogicExecutor supplierFunction) { } protected boolean skipJobExecution() { - return systemIsNotReady() || jobDisabled() || multiNodeEnabled(); + return !manualJobExecution() && (systemIsNotReady() || jobDisabled() || multiNodeEnabled()); } private void createJobExecution() { @@ -103,6 +104,10 @@ private void completeJobExecution(JobStatus status, String... message) { jobRepository.save(entity); } + private boolean manualJobExecution() { + return TRUE.equals(MdcUtils.get(MdcParameter.MANUAL_JOB_EXECUTION)); + } + private boolean systemIsNotReady() { SystemAttributeEntity systemAttribute = systemAttributeRepository.getSystemStatus().orElseThrow(); return !SystemStatus.OK.name().equals(systemAttribute.getValue()); diff --git a/code/gms-backend/src/main/java/io/github/gms/common/db/converter/EncryptedFieldConverter.java b/code/gms-backend/src/main/java/io/github/gms/common/db/converter/EncryptedFieldConverter.java index 57059751..bee3765a 100644 --- a/code/gms-backend/src/main/java/io/github/gms/common/db/converter/EncryptedFieldConverter.java +++ b/code/gms-backend/src/main/java/io/github/gms/common/db/converter/EncryptedFieldConverter.java @@ -18,12 +18,14 @@ import java.security.NoSuchAlgorithmException; import java.util.Base64; +import static io.github.gms.common.util.Constants.TRUE; + /** * @author Peter Szrnka * @since 1.0 */ @Component -@ConditionalOnProperty(value = "config.encryption.enable", havingValue = "true", matchIfMissing = true) +@ConditionalOnProperty(value = "config.encryption.enable", havingValue = TRUE, matchIfMissing = true) public class EncryptedFieldConverter implements AttributeConverter { private static final int AUTHENTICATION_TAG_LENGTH = 128; diff --git a/code/gms-backend/src/main/java/io/github/gms/common/dto/SimpleResponseDto.java b/code/gms-backend/src/main/java/io/github/gms/common/dto/SimpleResponseDto.java index 7a6dc4a1..faed6651 100644 --- a/code/gms-backend/src/main/java/io/github/gms/common/dto/SimpleResponseDto.java +++ b/code/gms-backend/src/main/java/io/github/gms/common/dto/SimpleResponseDto.java @@ -20,5 +20,6 @@ public class SimpleResponseDto implements Serializable { @Serial private static final long serialVersionUID = -5324564162143551148L; + @Builder.Default private boolean success = true; } diff --git a/code/gms-backend/src/main/java/io/github/gms/common/enums/MdcParameter.java b/code/gms-backend/src/main/java/io/github/gms/common/enums/MdcParameter.java index e3bcd792..b4953411 100644 --- a/code/gms-backend/src/main/java/io/github/gms/common/enums/MdcParameter.java +++ b/code/gms-backend/src/main/java/io/github/gms/common/enums/MdcParameter.java @@ -10,6 +10,7 @@ @Getter public enum MdcParameter { + MANUAL_JOB_EXECUTION("manualJobExecution", false), CORRELATION_ID("correlationId", false), USER_ID(Constants.USER_ID), USER_NAME("userName"), diff --git a/code/gms-backend/src/main/java/io/github/gms/common/enums/SystemProperty.java b/code/gms-backend/src/main/java/io/github/gms/common/enums/SystemProperty.java index 6e3e6a90..c118da4b 100644 --- a/code/gms-backend/src/main/java/io/github/gms/common/enums/SystemProperty.java +++ b/code/gms-backend/src/main/java/io/github/gms/common/enums/SystemProperty.java @@ -9,6 +9,7 @@ import static io.github.gms.common.enums.PropertyType.*; import static io.github.gms.common.enums.SystemPropertyCategory.*; +import static io.github.gms.common.util.Constants.TRUE; /** * @author Peter Szrnka @@ -34,21 +35,21 @@ public enum SystemProperty { JOB_OLD_EVENT_LIMIT(JOB, STRING, "180;d"), ENABLE_MULTI_NODE(JOB, BOOLEAN, "false"), EVENT_MAINTENANCE_RUNNER_CONTAINER_ID(JOB, STRING, ""), - EVENT_MAINTENANCE_JOB_ENABLED(JOB, BOOLEAN, "true"), + EVENT_MAINTENANCE_JOB_ENABLED(JOB, BOOLEAN, TRUE), JOB_MAINTENANCE_RUNNER_CONTAINER_ID(JOB, STRING, ""), - JOB_MAINTENANCE_JOB_ENABLED(JOB, BOOLEAN, "true"), + JOB_MAINTENANCE_JOB_ENABLED(JOB, BOOLEAN, TRUE), KEYSTORE_CLEANUP_RUNNER_CONTAINER_ID(JOB, STRING, ""), - KEYSTORE_CLEANUP_JOB_ENABLED(JOB, BOOLEAN, "true"), + KEYSTORE_CLEANUP_JOB_ENABLED(JOB, BOOLEAN, TRUE), LDAP_SYNC_RUNNER_CONTAINER_ID(JOB, STRING, ""), - LDAP_SYNC_JOB_ENABLED(JOB, BOOLEAN, "true"), + LDAP_SYNC_JOB_ENABLED(JOB, BOOLEAN, TRUE), MESSAGE_CLEANUP_RUNNER_CONTAINER_ID(JOB, STRING, ""), - MESSAGE_CLEANUP_JOB_ENABLED(JOB, BOOLEAN, "true"), + MESSAGE_CLEANUP_JOB_ENABLED(JOB, BOOLEAN, TRUE), SECRET_ROTATION_RUNNER_CONTAINER_ID(JOB, STRING, ""), - SECRET_ROTATION_JOB_ENABLED(JOB, BOOLEAN, "true"), - USER_ANONYMIZATION_JOB_ENABLED(JOB, BOOLEAN, "true"), + SECRET_ROTATION_JOB_ENABLED(JOB, BOOLEAN, TRUE), + USER_ANONYMIZATION_JOB_ENABLED(JOB, BOOLEAN, TRUE), USER_ANONYMIZATION_RUNNER_CONTAINER_ID(JOB, STRING, ""), USER_DELETION_RUNNER_CONTAINER_ID(JOB, STRING, ""), - USER_DELETION_JOB_ENABLED(JOB, BOOLEAN, "true"), + USER_DELETION_JOB_ENABLED(JOB, BOOLEAN, TRUE), // Other configurations ENABLE_AUTOMATIC_LOGOUT(GENERAL, BOOLEAN, "false"), AUTOMATIC_LOGOUT_TIME_IN_MINUTES(GENERAL, INTEGER, "15", value -> Integer.parseInt(value) >= 15); diff --git a/code/gms-backend/src/main/java/io/github/gms/common/enums/TimeUnit.java b/code/gms-backend/src/main/java/io/github/gms/common/enums/TimeUnit.java index 811e2fee..c738f0b1 100644 --- a/code/gms-backend/src/main/java/io/github/gms/common/enums/TimeUnit.java +++ b/code/gms-backend/src/main/java/io/github/gms/common/enums/TimeUnit.java @@ -13,6 +13,7 @@ */ public enum TimeUnit { + MINUTE("m", ChronoUnit.MINUTES), HOUR("h", ChronoUnit.HOURS), DAY("d", ChronoUnit.DAYS), WEEK("w", ChronoUnit.WEEKS), diff --git a/code/gms-backend/src/main/java/io/github/gms/job/ManualJobExecutionController.java b/code/gms-backend/src/main/java/io/github/gms/job/ManualJobExecutionController.java new file mode 100644 index 00000000..bc363ada --- /dev/null +++ b/code/gms-backend/src/main/java/io/github/gms/job/ManualJobExecutionController.java @@ -0,0 +1,62 @@ +package io.github.gms.job; + +import io.github.gms.common.abstraction.AbstractJob; +import io.github.gms.common.abstraction.GmsController; +import io.github.gms.common.enums.EventTarget; +import io.github.gms.common.enums.MdcParameter; +import io.github.gms.common.types.AuditTarget; +import io.github.gms.common.util.MdcUtils; +import io.github.gms.job.model.UrlConfiguration; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.http.ResponseEntity; +import org.springframework.lang.NonNull; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static io.github.gms.common.util.Constants.ROLE_ADMIN; +import static io.github.gms.common.util.Constants.TRUE; + +/** + * @author Peter Szrnka + * @since 1.0 + */ +@RestController +@RequiredArgsConstructor +@PreAuthorize(ROLE_ADMIN) +@AuditTarget(EventTarget.ANNOUNCEMENT) +@RequestMapping("/secure/job_execution") +public class ManualJobExecutionController implements GmsController { + + private final ApplicationContext applicationContext; + + @GetMapping("/{jobName}") + public ResponseEntity runJobByName(@PathVariable("jobName") String jobName) { + UrlConfiguration urlConfiguration = UrlConfiguration.fromUrl(jobName); + + if (urlConfiguration == null) { + return ResponseEntity.notFound().build(); + } + + return runJob(urlConfiguration.getClazz()); + } + + private ResponseEntity runJob(@NonNull Class clazz) { + MdcUtils.put(MdcParameter.MANUAL_JOB_EXECUTION, TRUE); + + try { + T job = applicationContext.getBean(clazz); + job.run(); + + MdcUtils.remove(MdcParameter.MANUAL_JOB_EXECUTION); + return ResponseEntity.ok().build(); + } catch (NoSuchBeanDefinitionException e) { + MdcUtils.remove(MdcParameter.MANUAL_JOB_EXECUTION); + return ResponseEntity.notFound().build(); + } + } +} diff --git a/code/gms-backend/src/main/java/io/github/gms/job/model/UrlConfiguration.java b/code/gms-backend/src/main/java/io/github/gms/job/model/UrlConfiguration.java new file mode 100644 index 00000000..c1a7b379 --- /dev/null +++ b/code/gms-backend/src/main/java/io/github/gms/job/model/UrlConfiguration.java @@ -0,0 +1,35 @@ +package io.github.gms.job.model; + +import io.github.gms.common.abstraction.AbstractJob; +import io.github.gms.job.*; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * @author Peter Szrnka + * @since 1.0 + */ +@Getter +@RequiredArgsConstructor +public enum UrlConfiguration { + EVENT_MAINTENANCE(UrlConstants.EVENT_MAINTENANCE, EventMaintenanceJob.class), + GENERATED_KEYSTORE_CLEANUP(UrlConstants.GENERATED_KEYSTORE_CLEANUP, GeneratedKeystoreCleanupJob.class), + JOB_MAINTENANCE(UrlConstants.JOB_MAINTENANCE, JobMaintenanceJob.class), + MESSAGE_CLEANUP(UrlConstants.MESSAGE_CLEANUP, MessageCleanupJob.class), + SECRET_ROTATION(UrlConstants.SECRET_ROTATION, SecretRotationJob.class), + USER_ANONYMIZATION(UrlConstants.USER_ANONYMIZATION, UserAnonymizationJob.class), + USER_DELETION(UrlConstants.USER_DELETION, UserDeletionJob.class), + LDAP_USER_SYNC(UrlConstants.LDAP_USER_SYNC, LdapUserSyncJob.class); + + private final String url; + private final Class clazz; + + public static UrlConfiguration fromUrl(String url) { + return Arrays.stream(UrlConfiguration.values()) + .filter(urlConfiguration -> urlConfiguration.url.equals(url)) + .findFirst() + .orElse(null); + } +} \ No newline at end of file diff --git a/code/gms-backend/src/main/java/io/github/gms/job/model/UrlConstants.java b/code/gms-backend/src/main/java/io/github/gms/job/model/UrlConstants.java new file mode 100644 index 00000000..cf242ae1 --- /dev/null +++ b/code/gms-backend/src/main/java/io/github/gms/job/model/UrlConstants.java @@ -0,0 +1,16 @@ +package io.github.gms.job.model; + +/** + * @author Peter Szrnka + * @since 1.0 + */ +public interface UrlConstants { + String EVENT_MAINTENANCE = "event_maintenance"; + String GENERATED_KEYSTORE_CLEANUP = "generated_keystore_cleanup"; + String JOB_MAINTENANCE = "job_maintenance"; + String MESSAGE_CLEANUP = "message_cleanup"; + String SECRET_ROTATION = "secret_rotation"; + String USER_ANONYMIZATION = "user_anonymization"; + String USER_DELETION = "user_deletion"; + String LDAP_USER_SYNC = "ldap_user_sync"; +} diff --git a/code/gms-backend/src/test/java/io/github/gms/ClickJackingProtectionTest.java b/code/gms-backend/src/test/java/io/github/gms/ClickJackingProtectionTest.java index 378405e7..6c28a543 100644 --- a/code/gms-backend/src/test/java/io/github/gms/ClickJackingProtectionTest.java +++ b/code/gms-backend/src/test/java/io/github/gms/ClickJackingProtectionTest.java @@ -18,7 +18,7 @@ class ClickJackingProtectionTest extends AbstractIntegrationTest { @Test - void testClickJackingProtection() throws Exception { + void testClickJackingProtection() { // arrange HttpEntity requestEntity = new HttpEntity<>(null); diff --git a/code/gms-backend/src/test/java/io/github/gms/IntegrationAnnotationCheckerTest.java b/code/gms-backend/src/test/java/io/github/gms/IntegrationAnnotationCheckerTest.java index 0681ae78..4b5d44d0 100644 --- a/code/gms-backend/src/test/java/io/github/gms/IntegrationAnnotationCheckerTest.java +++ b/code/gms-backend/src/test/java/io/github/gms/IntegrationAnnotationCheckerTest.java @@ -119,9 +119,11 @@ private static Map getAllControllerClasses(boolean securityTe ClassData classData = new ClassData(); Set controllerMethods = Stream.of(controller.getDeclaredMethods()) + .filter(method -> Modifier.isPublic(method.getModifiers())) .filter(method -> !securityTestCheck || method.getAnnotation(SkipSecurityTestCheck.class) == null) .map(Method::getName) - .filter(name -> !name.startsWith("lambda$")).collect(Collectors.toSet()); + .filter(name -> !name.startsWith("lambda$") && !name.startsWith("set")) + .collect(Collectors.toSet()); controllerMethods.addAll(Stream.of(controller.getSuperclass().getDeclaredMethods()) .filter(method -> Modifier.isPublic(method.getModifiers())) diff --git a/code/gms-backend/src/test/java/io/github/gms/job/EventMaintenanceJobTest.java b/code/gms-backend/src/test/java/io/github/gms/job/EventMaintenanceJobTest.java index 181c76b2..7835b033 100644 --- a/code/gms-backend/src/test/java/io/github/gms/job/EventMaintenanceJobTest.java +++ b/code/gms-backend/src/test/java/io/github/gms/job/EventMaintenanceJobTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.MockedStatic; +import org.slf4j.MDC; import org.springframework.test.util.ReflectionTestUtils; import java.time.Clock; @@ -60,6 +61,8 @@ public void setup() { ReflectionTestUtils.setField(job, "jobRepository", jobRepository); ReflectionTestUtils.setField(job, "systemAttributeRepository", systemAttributeRepository); addAppender(EventMaintenanceJob.class); + + MDC.clear(); } @Test diff --git a/code/gms-backend/src/test/java/io/github/gms/job/GeneratedKeystoreCleanupJobTest.java b/code/gms-backend/src/test/java/io/github/gms/job/GeneratedKeystoreCleanupJobTest.java index 3616e032..aa4b7ee8 100644 --- a/code/gms-backend/src/test/java/io/github/gms/job/GeneratedKeystoreCleanupJobTest.java +++ b/code/gms-backend/src/test/java/io/github/gms/job/GeneratedKeystoreCleanupJobTest.java @@ -12,6 +12,7 @@ import io.github.gms.util.TestUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.slf4j.MDC; import org.springframework.test.util.ReflectionTestUtils; import java.time.Clock; @@ -60,6 +61,8 @@ public void setup() { ReflectionTestUtils.setField(job, "systemAttributeRepository", systemAttributeRepository); addAppender(GeneratedKeystoreCleanupJob.class); + + MDC.clear(); } @Test diff --git a/code/gms-backend/src/test/java/io/github/gms/job/JobMaintenanceJobTest.java b/code/gms-backend/src/test/java/io/github/gms/job/JobMaintenanceJobTest.java index 69620c87..506c107a 100644 --- a/code/gms-backend/src/test/java/io/github/gms/job/JobMaintenanceJobTest.java +++ b/code/gms-backend/src/test/java/io/github/gms/job/JobMaintenanceJobTest.java @@ -10,6 +10,7 @@ import io.github.gms.util.TestUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.slf4j.MDC; import org.springframework.test.util.ReflectionTestUtils; import java.time.Clock; @@ -53,6 +54,8 @@ public void setup() { ReflectionTestUtils.setField(job, "jobRepository", jobRepository); ReflectionTestUtils.setField(job, "systemAttributeRepository", systemAttributeRepository); addAppender(JobMaintenanceJob.class); + + MDC.clear(); } @Test diff --git a/code/gms-backend/src/test/java/io/github/gms/job/LdapUserSyncJobTest.java b/code/gms-backend/src/test/java/io/github/gms/job/LdapUserSyncJobTest.java index d420b893..a1ad49fb 100644 --- a/code/gms-backend/src/test/java/io/github/gms/job/LdapUserSyncJobTest.java +++ b/code/gms-backend/src/test/java/io/github/gms/job/LdapUserSyncJobTest.java @@ -14,6 +14,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.slf4j.MDC; import org.springframework.data.util.Pair; import org.springframework.test.util.ReflectionTestUtils; @@ -63,6 +64,8 @@ public void setup() { ReflectionTestUtils.setField(job, "systemAttributeRepository", systemAttributeRepository); addAppender(LdapUserSyncJob.class); + + MDC.clear(); } @Test diff --git a/code/gms-backend/src/test/java/io/github/gms/job/ManualJobExecutionControllerIntegrationTest.java b/code/gms-backend/src/test/java/io/github/gms/job/ManualJobExecutionControllerIntegrationTest.java new file mode 100644 index 00000000..e4c0a8f3 --- /dev/null +++ b/code/gms-backend/src/test/java/io/github/gms/job/ManualJobExecutionControllerIntegrationTest.java @@ -0,0 +1,69 @@ +package io.github.gms.job; + +import io.github.gms.abstraction.AbstractIntegrationTest; +import io.github.gms.abstraction.GmsControllerIntegrationTest; +import io.github.gms.common.TestedClass; +import io.github.gms.common.TestedMethod; +import io.github.gms.job.model.UrlConstants; +import io.github.gms.util.TestUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static io.github.gms.util.TestConstants.TAG_INTEGRATION_TEST; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * @author Peter Szrnka + * @since 1.0 + */ +@Tag(TAG_INTEGRATION_TEST) +@TestedClass(ManualJobExecutionController.class) +class ManualJobExecutionControllerIntegrationTest extends AbstractIntegrationTest implements GmsControllerIntegrationTest { + + @Override + @BeforeEach + public void setup() { + gmsUser = TestUtils.createGmsAdminUser(); + jwt = jwtService.generateJwt(TestUtils.createJwtUserRequest(gmsUser)); + } + + @ParameterizedTest + @TestedMethod("runJobByName") + @MethodSource("jobNameTestData") + void runJobByName_whenInputIsProvided_thenReturnExpectedStatus(String urlPath, HttpStatus expectedStatus) { + assertByUrl("/" + urlPath, expectedStatus); + } + + private static Object[][] jobNameTestData() { + return new Object[][] { + {UrlConstants.JOB_MAINTENANCE, HttpStatus.OK}, + {UrlConstants.EVENT_MAINTENANCE, HttpStatus.OK}, + {UrlConstants.GENERATED_KEYSTORE_CLEANUP, HttpStatus.OK}, + {UrlConstants.MESSAGE_CLEANUP, HttpStatus.OK}, + {UrlConstants.SECRET_ROTATION, HttpStatus.OK}, + {UrlConstants.USER_ANONYMIZATION, HttpStatus.OK}, + {UrlConstants.USER_DELETION, HttpStatus.OK}, + {UrlConstants.LDAP_USER_SYNC, HttpStatus.NOT_FOUND}, + {"/invalid_job", HttpStatus.NOT_FOUND} + }; + } + + private void assertByUrl(String url, HttpStatus expectedStatus) { + // arrange + HttpEntity requestEntity = new HttpEntity<>(TestUtils.getHttpHeaders(jwt)); + + // act + String path = "/secure/job_execution"; + ResponseEntity response = executeHttpGet(path + url, requestEntity, Void.class); + + // assert + assertNotNull(response); + assertEquals(expectedStatus, response.getStatusCode()); + } +} diff --git a/code/gms-backend/src/test/java/io/github/gms/job/ManualJobExecutionControllerTest.java b/code/gms-backend/src/test/java/io/github/gms/job/ManualJobExecutionControllerTest.java new file mode 100644 index 00000000..7789ec8f --- /dev/null +++ b/code/gms-backend/src/test/java/io/github/gms/job/ManualJobExecutionControllerTest.java @@ -0,0 +1,70 @@ +package io.github.gms.job; + +import io.github.gms.abstraction.AbstractUnitTest; +import io.github.gms.common.enums.MdcParameter; +import io.github.gms.common.util.MdcUtils; +import io.github.gms.job.model.UrlConstants; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.ResponseEntity; + +import static io.github.gms.common.util.Constants.TRUE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +/** + * @author Peter Szrnka + * @since 1.0 + */ +class ManualJobExecutionControllerTest extends AbstractUnitTest { + + @Mock + private ApplicationContext applicationContext; + @InjectMocks + private ManualJobExecutionController manualJobExecutionController; + + @Test + void runJobByName_whenJobNotFound_thenReturnNotFound() { + // arrange + String jobName = "jobName"; + + // act + ResponseEntity response = manualJobExecutionController.runJobByName(jobName); + + // assert + assertEquals(HttpStatusCode.valueOf(404), response.getStatusCode()); + } + + @Test + void runJobByName_whenJobFound_thenReturnOk() { + try (MockedStatic mockedStatic = mockStatic(MdcUtils.class)) { + // arrange + mockedStatic.when(() -> MdcUtils.put(MdcParameter.MANUAL_JOB_EXECUTION, TRUE)).thenCallRealMethod(); + when(applicationContext.getBean(EventMaintenanceJob.class)).thenReturn(mock(EventMaintenanceJob.class)); + + // act + ResponseEntity response = manualJobExecutionController.runJobByName(UrlConstants.EVENT_MAINTENANCE); + + // assert + assertEquals(HttpStatusCode.valueOf(200), response.getStatusCode()); + mockedStatic.verify(() -> MdcUtils.put(MdcParameter.MANUAL_JOB_EXECUTION, TRUE), times(1)); + } + } + + @Test + void runJobByName_whenJobBeanNotFound_thenReturnNotFound() { + // arrange + when(applicationContext.getBean(EventMaintenanceJob.class)).thenThrow(NoSuchBeanDefinitionException.class); + + // act + ResponseEntity response = manualJobExecutionController.runJobByName(UrlConstants.EVENT_MAINTENANCE); + + // assert + assertEquals(HttpStatusCode.valueOf(404), response.getStatusCode()); + } +} \ No newline at end of file diff --git a/code/gms-backend/src/test/java/io/github/gms/job/ManualJobExecutionControllerUserRoleSecurityTest.java b/code/gms-backend/src/test/java/io/github/gms/job/ManualJobExecutionControllerUserRoleSecurityTest.java new file mode 100644 index 00000000..c8908dc4 --- /dev/null +++ b/code/gms-backend/src/test/java/io/github/gms/job/ManualJobExecutionControllerUserRoleSecurityTest.java @@ -0,0 +1,51 @@ +package io.github.gms.job; + +import io.github.gms.abstraction.AbstractUserRoleSecurityTest; +import io.github.gms.common.TestedClass; +import io.github.gms.common.TestedMethod; +import io.github.gms.job.model.UrlConstants; +import io.github.gms.util.TestUtils; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static io.github.gms.util.TestConstants.TAG_SECURITY_TEST; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Peter Szrnka + * @since 1.0 + */ +@Tag(TAG_SECURITY_TEST) +@TestedClass(ManualJobExecutionController.class) +class ManualJobExecutionControllerUserRoleSecurityTest extends AbstractUserRoleSecurityTest { + + public ManualJobExecutionControllerUserRoleSecurityTest() { + super("/secure/job_execution"); + } + + @ParameterizedTest + @TestedMethod("runJobByName") + @ValueSource(strings = { + UrlConstants.JOB_MAINTENANCE, + UrlConstants.EVENT_MAINTENANCE, + UrlConstants.GENERATED_KEYSTORE_CLEANUP, + UrlConstants.LDAP_USER_SYNC, + UrlConstants.MESSAGE_CLEANUP, + UrlConstants.SECRET_ROTATION, + UrlConstants.USER_ANONYMIZATION, + UrlConstants.USER_DELETION + }) + void runJobByName_whenUserIsNull_thenReturnHttp403(String urlPath) { + HttpEntity requestEntity = new HttpEntity<>(TestUtils.getHttpHeaders(jwt)); + + // act + ResponseEntity response = executeHttpGet(urlPrefix + "/" + urlPath, requestEntity, Void.class); + + // assert + assertEquals(HttpStatus.FORBIDDEN, response.getStatusCode()); + } +} diff --git a/code/gms-backend/src/test/java/io/github/gms/job/MessageCleanupJobTest.java b/code/gms-backend/src/test/java/io/github/gms/job/MessageCleanupJobTest.java index a40cee7a..6f32f69e 100644 --- a/code/gms-backend/src/test/java/io/github/gms/job/MessageCleanupJobTest.java +++ b/code/gms-backend/src/test/java/io/github/gms/job/MessageCleanupJobTest.java @@ -12,6 +12,7 @@ import io.github.gms.util.TestUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.slf4j.MDC; import org.springframework.test.util.ReflectionTestUtils; import java.time.Clock; @@ -58,6 +59,8 @@ public void setup() { ReflectionTestUtils.setField(job, "systemAttributeRepository", systemAttributeRepository); addAppender(MessageCleanupJob.class); + + MDC.clear(); } @Test diff --git a/code/gms-backend/src/test/java/io/github/gms/job/SecretRotationJobTest.java b/code/gms-backend/src/test/java/io/github/gms/job/SecretRotationJobTest.java index 80c647a0..36255576 100644 --- a/code/gms-backend/src/test/java/io/github/gms/job/SecretRotationJobTest.java +++ b/code/gms-backend/src/test/java/io/github/gms/job/SecretRotationJobTest.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.slf4j.MDC; import org.springframework.test.util.ReflectionTestUtils; import java.time.*; @@ -62,6 +63,8 @@ public void setup() { ReflectionTestUtils.setField(job, "systemAttributeRepository", systemAttributeRepository); addAppender(SecretRotationJob.class); + + MDC.clear(); } @Test diff --git a/code/gms-backend/src/test/java/io/github/gms/job/UserAnonymizationJobTest.java b/code/gms-backend/src/test/java/io/github/gms/job/UserAnonymizationJobTest.java index e6faab71..4979a1c3 100644 --- a/code/gms-backend/src/test/java/io/github/gms/job/UserAnonymizationJobTest.java +++ b/code/gms-backend/src/test/java/io/github/gms/job/UserAnonymizationJobTest.java @@ -3,15 +3,16 @@ import io.github.gms.abstraction.AbstractLoggingUnitTest; import io.github.gms.common.enums.SystemProperty; import io.github.gms.common.enums.SystemStatus; -import io.github.gms.functions.maintenance.user.UserAnonymizationService; import io.github.gms.functions.maintenance.job.JobEntity; import io.github.gms.functions.maintenance.job.JobRepository; +import io.github.gms.functions.maintenance.user.UserAnonymizationService; import io.github.gms.functions.setup.SystemAttributeRepository; import io.github.gms.functions.system.SystemService; import io.github.gms.functions.systemproperty.SystemPropertyService; import io.github.gms.util.TestUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.slf4j.MDC; import org.springframework.test.util.ReflectionTestUtils; import java.time.Clock; @@ -57,6 +58,8 @@ public void setup() { ReflectionTestUtils.setField(job, "jobRepository", jobRepository); ReflectionTestUtils.setField(job, "systemAttributeRepository", systemAttributeRepository); addAppender(UserAnonymizationJob.class); + + MDC.clear(); } @Test diff --git a/code/gms-backend/src/test/java/io/github/gms/job/UserDeletionJobTest.java b/code/gms-backend/src/test/java/io/github/gms/job/UserDeletionJobTest.java index 9d4e91a2..253e438e 100644 --- a/code/gms-backend/src/test/java/io/github/gms/job/UserDeletionJobTest.java +++ b/code/gms-backend/src/test/java/io/github/gms/job/UserDeletionJobTest.java @@ -13,6 +13,7 @@ import io.github.gms.util.TestUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.slf4j.MDC; import org.springframework.test.util.ReflectionTestUtils; import java.time.Clock; @@ -61,6 +62,8 @@ public void setup() { ReflectionTestUtils.setField(job, "jobRepository", jobRepository); ReflectionTestUtils.setField(job, "systemAttributeRepository", systemAttributeRepository); addAppender(UserDeletionJob.class); + + MDC.clear(); } @Test diff --git a/code/gms-frontend/src/app/common/components/nav-back/nav-back.component.ts b/code/gms-frontend/src/app/common/components/nav-back/nav-back.component.ts index 87d5d708..abd858ba 100644 --- a/code/gms-frontend/src/app/common/components/nav-back/nav-back.component.ts +++ b/code/gms-frontend/src/app/common/components/nav-back/nav-back.component.ts @@ -1,10 +1,9 @@ -import { NgFor, NgIf } from '@angular/common'; import { Component, Input } from '@angular/core'; import { RouterLink } from '@angular/router'; import { AngularMaterialModule } from '../../../angular-material-module'; import { NavButtonVisibilityPipe } from '../pipes/nav-button-visibility.pipe'; -import { ButtonConfig } from './button-config'; import { TranslatorModule } from '../pipes/translator/translator.module'; +import { ButtonConfig } from './button-config'; /** * @author Peter Szrnka @@ -14,7 +13,6 @@ import { TranslatorModule } from '../pipes/translator/translator.module'; imports: [ AngularMaterialModule, NavButtonVisibilityPipe, - NgIf, NgFor, RouterLink, TranslatorModule ], diff --git a/code/gms-frontend/src/app/components/job/job-detail-list.component.html b/code/gms-frontend/src/app/components/job/job-detail-list.component.html index 7cdcbee6..0ed939d3 100644 --- a/code/gms-frontend/src/app/components/job/job-detail-list.component.html +++ b/code/gms-frontend/src/app/components/job/job-detail-list.component.html @@ -3,58 +3,72 @@

{{ 'job.title' | translate }}

+ + +
{{ 'job.execution.info' | translate }}
+ @for (item of job_execution_config; track $index) { + + } + + @if (authMode$ | async; as authMode) { + + } +
+
+ @if (error) { -
{{ 'messages.error' | translate }}: {{error}}
+
{{ 'messages.error' | translate }}: {{error}}
} @if (!error) { - - - - {{ 'tables.filter' | translate }} - - + + + + {{ 'tables.filter' | translate }} + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ID {{element.id}} {{ 'tables.name' | translate }} {{element.name}} Correlation Id {{element.correlationId}} {{ 'tables.status' | translate }} - {{element.status}} - {{ 'job.duration' | translate }} - {{element.duration}} - {{ 'tables.creationDate' | translate }} {{element.creationDate | momentPipe:'yyyy.MM.DD. HH:mm:ss'}} {{ 'messages.message' | translate }} - {{element.message}} -
- + + + + ID + {{element.id}} + + + {{ 'tables.name' | translate }} + {{element.name}} + + + Correlation Id + {{element.correlationId}} + + + {{ 'tables.status' | translate }} + + {{element.status}} + + + + {{ 'job.duration' | translate }} + + {{element.duration}} + + + + {{ 'tables.creationDate' | translate }} + {{element.creationDate | momentPipe:'yyyy.MM.DD. + HH:mm:ss'}} + + + {{ 'messages.message' | translate }} + + {{element.message}} + + + + -
-
+
+
} \ No newline at end of file diff --git a/code/gms-frontend/src/app/components/job/job-detail-list.component.spec.ts b/code/gms-frontend/src/app/components/job/job-detail-list.component.spec.ts index a0598c9d..2a8b0b2d 100644 --- a/code/gms-frontend/src/app/components/job/job-detail-list.component.spec.ts +++ b/code/gms-frontend/src/app/components/job/job-detail-list.component.spec.ts @@ -1,14 +1,18 @@ import { HttpErrorResponse } from "@angular/common/http"; import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from "@angular/compiler"; import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { MatSnackBar } from "@angular/material/snack-bar"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { ActivatedRoute, Data, Router } from "@angular/router"; -import { of, throwError } from "rxjs"; +import { of, ReplaySubject, throwError } from "rxjs"; import { AngularMaterialModule } from "../../angular-material-module"; import { MomentPipe } from "../../common/components/pipes/date-formatter.pipe"; +import { TranslatorModule } from "../../common/components/pipes/translator/translator.module"; +import { SharedDataService } from "../../common/service/shared-data-service"; +import { TranslatorService } from "../../common/service/translator-service"; import { JobDetailListComponent } from "./job-detail-list.component"; import { JobDetail } from "./model/job-detail.model"; -import { TranslatorModule } from "../../common/components/pipes/translator/translator.module"; +import { JobDetailService } from "./service/job-detail.service"; /** * @author Peter Szrnka @@ -19,6 +23,10 @@ describe('JobDetailListComponent', () => { // Injected services let router: any; let activatedRoute : any = {}; + let jobDetailService: any; + let snackbar: any; + let translatorService: any; + let authModeSubject = new ReplaySubject(); const configureTestBed = () => { TestBed.configureTestingModule({ @@ -26,13 +34,18 @@ describe('JobDetailListComponent', () => { schemas: [ CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA ], providers: [ { provide : Router, useValue : router }, - { provide : ActivatedRoute, useClass : activatedRoute } + { provide : ActivatedRoute, useClass : activatedRoute }, + { provide : JobDetailService, useValue : jobDetailService }, + { provide : MatSnackBar, useValue : snackbar }, + { provide : SharedDataService, useValue : { authModeSubject$: authModeSubject } }, + { provide : TranslatorService, useValue : translatorService } ] }); fixture = TestBed.createComponent(JobDetailListComponent); component = fixture.componentInstance; fixture.detectChanges(); + authModeSubject.next('db'); }; @@ -55,6 +68,16 @@ describe('JobDetailListComponent', () => { queryParams : {} } }; + + jobDetailService = { + startManualExecution : jest.fn().mockReturnValue(of({})) + }; + snackbar = { + open : jest.fn() + }; + translatorService = { + translate : jest.fn().mockReturnValue('Job executed successfully') + }; }); it('Should handle resolver error', () => { @@ -84,4 +107,21 @@ describe('JobDetailListComponent', () => { expect(component).toBeTruthy(); expect(router.navigateByUrl).toHaveBeenCalled(); }); + + it('executeJob when jobUrl is provided', () => { + configureTestBed(); + + component.executeJob('generated_keystore_cleanup'); + + expect(jobDetailService.startManualExecution).toHaveBeenCalled(); + }); + + it('executeJob when jobUrl is invalid then handle error', () => { + jobDetailService.startManualExecution = jest.fn().mockReturnValue(throwError(() => new HttpErrorResponse({ error : new Error("OOPS!"), status : 500, statusText: "OOPS!"}))); + configureTestBed(); + + component.executeJob('invalid_job_url'); + + expect(jobDetailService.startManualExecution).toHaveBeenCalled(); + }); }); \ No newline at end of file diff --git a/code/gms-frontend/src/app/components/job/job-detail-list.component.ts b/code/gms-frontend/src/app/components/job/job-detail-list.component.ts index 603c9da0..c84b9d32 100644 --- a/code/gms-frontend/src/app/components/job/job-detail-list.component.ts +++ b/code/gms-frontend/src/app/components/job/job-detail-list.component.ts @@ -1,27 +1,44 @@ +import { CommonModule } from "@angular/common"; import { Component, OnInit } from "@angular/core"; +import { MatSnackBar } from "@angular/material/snack-bar"; import { MatTableDataSource } from "@angular/material/table"; import { ActivatedRoute, Router } from "@angular/router"; -import { catchError } from "rxjs"; +import { catchError, Observable } from "rxjs"; import { AngularMaterialModule } from "../../angular-material-module"; import { NavBackComponent } from "../../common/components/nav-back/nav-back.component"; import { MomentPipe } from "../../common/components/pipes/date-formatter.pipe"; -import { JobDetail } from "./model/job-detail.model"; import { TranslatorModule } from "../../common/components/pipes/translator/translator.module"; +import { SharedDataService } from "../../common/service/shared-data-service"; +import { TranslatorService } from "../../common/service/translator-service"; +import { JobDetail } from "./model/job-detail.model"; +import { JobDetailService } from "./service/job-detail.service"; + +const MANUAL_JOB_EXECUTION_CONFIG = [ + { label: 'job.button.event.maintenance', url : 'event_maintenance' }, + { label: 'job.button.keystore.cleanup', url : 'generated_keystore_cleanup' }, + { label: 'job.button.old.job.log.cleanup', url : 'job_maintenance' }, + { label: 'job.button.message.cleanup', url : 'message_cleanup' }, + { label: 'job.button.secret.rotation', url : 'secret_rotation' }, + { label: 'job.button.user.anonymization', url : 'user_anonymization' }, + { label: 'job.button.user.deletion', url : 'user_deletion' } +]; /** * @author Peter Szrnka */ @Component({ standalone: true, - imports: [AngularMaterialModule, NavBackComponent, MomentPipe, TranslatorModule], + imports: [AngularMaterialModule, CommonModule, NavBackComponent, MomentPipe, TranslatorModule], selector: 'job-detail-list', templateUrl: './job-detail-list.component.html' }) export class JobDetailListComponent implements OnInit { columns: string[] = ['id', 'name', 'correlationId', 'status', 'duration', 'creationDate', 'message']; + job_execution_config = MANUAL_JOB_EXECUTION_CONFIG; loading = true; + authMode$: Observable = this.sharedData.authModeSubject$; public datasource: MatTableDataSource; public error?: string; @@ -33,7 +50,11 @@ export class JobDetailListComponent implements OnInit { constructor( private readonly router: Router, - private readonly activatedRoute: ActivatedRoute) { + private readonly activatedRoute: ActivatedRoute, + private readonly sharedData: SharedDataService, + private readonly jobDetailService: JobDetailService, + private readonly snackbar : MatSnackBar, + private readonly translatorService: TranslatorService) { } ngOnInit(): void { @@ -48,6 +69,13 @@ export class JobDetailListComponent implements OnInit { }); } + executeJob(jobUrl: string) { + this.jobDetailService.startManualExecution(jobUrl).subscribe({ + next: () => this.snackbar.open(this.translatorService.translate('job.manual.execution.success')), + error: () => this.snackbar.open(this.translatorService.translate('job.manual.execution.error')) + }); + } + private initDefaultDataTable() { this.datasource = new MatTableDataSource([]); } diff --git a/code/gms-frontend/src/app/components/job/service/job-detail.service.spec.ts b/code/gms-frontend/src/app/components/job/service/job-detail.service.spec.ts index 55476abb..4febad0b 100644 --- a/code/gms-frontend/src/app/components/job/service/job-detail.service.spec.ts +++ b/code/gms-frontend/src/app/components/job/service/job-detail.service.spec.ts @@ -45,4 +45,19 @@ describe("JobDetailService", () => { req.flush(request); httpMock.verify(); }); + + it('startManualExecution when called then return http 200', () => { + // arrange + const expectedUrl = environment.baseUrl + "secure/job_execution/generated_keystore_cleanup"; + const mockResponse = {}; + + // act + service.startManualExecution('generated_keystore_cleanup').subscribe((res) => expect(res).toBe(mockResponse)); + + // assert + const req = httpMock.expectOne(expectedUrl); + expect(req.request.method).toBe('GET'); + req.flush(mockResponse); + httpMock.verify(); + }); }); \ No newline at end of file diff --git a/code/gms-frontend/src/app/components/job/service/job-detail.service.ts b/code/gms-frontend/src/app/components/job/service/job-detail.service.ts index 986abd34..7fa7296a 100644 --- a/code/gms-frontend/src/app/components/job/service/job-detail.service.ts +++ b/code/gms-frontend/src/app/components/job/service/job-detail.service.ts @@ -17,4 +17,8 @@ export class JobDetailService { return this.http.get(environment.baseUrl + `secure/job/list?direction=${paging.direction}&property=${paging.property}&page=${paging.page}&size=${paging.size}`, { withCredentials: true, headers : getHeaders() }); } + + startManualExecution(jobName: string): Observable { + return this.http.get(environment.baseUrl + `secure/job_execution/${jobName}`, { withCredentials: true, headers : getHeaders() }); + } } \ No newline at end of file diff --git a/code/gms-frontend/src/app/components/settings/settings-summary.component.ts b/code/gms-frontend/src/app/components/settings/settings-summary.component.ts index 783f835c..9329aaf0 100644 --- a/code/gms-frontend/src/app/components/settings/settings-summary.component.ts +++ b/code/gms-frontend/src/app/components/settings/settings-summary.component.ts @@ -13,17 +13,7 @@ export interface PasswordSettings { oldCredential: string | undefined, newCredential1: string | undefined, newCredential2: string | undefined -} - -/*const LANGUAGE_SETTINGS_EN = [ - { key: 'en', value: 'English' }, - { key: 'hu', value: 'Hungarian' } -]; - -const LANGUAGE_SETTINGS_HU = [ - { key: 'en', value: 'Angol' }, - { key: 'hu', value: 'Magyar' } -];*/ +}; /** * @author Peter Szrnka diff --git a/code/gms-frontend/src/assets/i18n/translations.json b/code/gms-frontend/src/assets/i18n/translations.json index 012b2b4c..853eb8c3 100644 --- a/code/gms-frontend/src/assets/i18n/translations.json +++ b/code/gms-frontend/src/assets/i18n/translations.json @@ -194,6 +194,18 @@ "job.title" : "Jobs", "job.duration" : "Duration (ms)", + "job.manual.execution.toggle": "Toggle manual job execution panel", + "job.execution.info": "Please select a job that you want to run manually.", + "job.button.event.maintenance" : "Event maintenance", + "job.button.keystore.cleanup" : "Generated keystore cleanup", + "job.button.old.job.log.cleanup" : "Delete old job log entries", + "job.button.message.cleanup" : "Message cleanup", + "job.button.secret.rotation" : "Secret rotation", + "job.button.user.anonymization" : "User anonymization", + "job.button.user.deletion" : "User deletion", + "job.button.ldapsync" : "LDAP user sync", + "job.manual.execution.success": "Job executed successfully!", + "job.manual.execution.error": "Job execution failed!", "systemProperties.category": "Category", @@ -490,6 +502,18 @@ "job.title" : "Munkafolyamatok", "job.duration" : "Futásidő (ms)", + "job.manual.execution.toggle": "Kézi munkafolyamat végrehajtás panel", + "job.execution.info": "Kérjük válasszon egy munkafolyamatot, amelyet kézzel szeretne futtatni.", + "job.button.event.maintenance" : "Esemény karbantartás", + "job.button.keystore.cleanup" : "Generált kulcstárak törlése", + "job.button.old.job.log.cleanup" : "Régi munkafolyamat napló bejegyzések törlése", + "job.button.message.cleanup" : "Üzenetek törlése", + "job.button.secret.rotation" : "Secret rotálás", + "job.button.user.anonymization" : "Felhasználó anonimizálás", + "job.button.user.deletion" : "Felhasználó törlés", + "job.button.ldapsync" : "LDAP felhasználó szinkronizálás", + "job.manual.execution.success": "Munkafolyamat sikeresen végrehajtva!", + "job.manual.execution.error": "Munkafolyamat végrehajtása sikertelen!", "systemProperties.category": "Kategória",