Skip to content

Commit

Permalink
Merge pull request #353 from peter-szrnka/352-gms-173-manual-job-exec…
Browse files Browse the repository at this point in the history
…ution-feature

GMS-173 manual job execution feature
  • Loading branch information
peter-szrnka authored Dec 6, 2024
2 parents 92a1b5c + e510d9a commit a00da66
Show file tree
Hide file tree
Showing 31 changed files with 536 additions and 83 deletions.
2 changes: 1 addition & 1 deletion code/gms-backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
<version>3.2.4</version>
<version>3.2.8</version>
</dependency>
<dependency>
<groupId>com.unboundid</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -73,7 +74,7 @@ protected void execute(BusinessLogicExecutor supplierFunction) {
}

protected boolean skipJobExecution() {
return systemIsNotReady() || jobDisabled() || multiNodeEnabled();
return !manualJobExecution() && (systemIsNotReady() || jobDisabled() || multiNodeEnabled());
}

private void createJobExecution() {
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> {

private static final int AUTHENTICATION_TAG_LENGTH = 128;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ public class SimpleResponseDto implements Serializable {
@Serial
private static final long serialVersionUID = -5324564162143551148L;

@Builder.Default
private boolean success = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/
public enum TimeUnit {

MINUTE("m", ChronoUnit.MINUTES),
HOUR("h", ChronoUnit.HOURS),
DAY("d", ChronoUnit.DAYS),
WEEK("w", ChronoUnit.WEEKS),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Void> runJobByName(@PathVariable("jobName") String jobName) {
UrlConfiguration urlConfiguration = UrlConfiguration.fromUrl(jobName);

if (urlConfiguration == null) {
return ResponseEntity.notFound().build();
}

return runJob(urlConfiguration.getClazz());
}

private <T extends AbstractJob> ResponseEntity<Void> runJob(@NonNull Class<T> 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<? extends AbstractJob> clazz;

public static UrlConfiguration fromUrl(String url) {
return Arrays.stream(UrlConfiguration.values())
.filter(urlConfiguration -> urlConfiguration.url.equals(url))
.findFirst()
.orElse(null);
}
}
Original file line number Diff line number Diff line change
@@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
class ClickJackingProtectionTest extends AbstractIntegrationTest {

@Test
void testClickJackingProtection() throws Exception {
void testClickJackingProtection() {
// arrange
HttpEntity<Void> requestEntity = new HttpEntity<>(null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,11 @@ private static Map<String, ClassData> getAllControllerClasses(boolean securityTe

ClassData classData = new ClassData();
Set<String> 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()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -60,6 +61,8 @@ public void setup() {
ReflectionTestUtils.setField(job, "jobRepository", jobRepository);
ReflectionTestUtils.setField(job, "systemAttributeRepository", systemAttributeRepository);
addAppender(EventMaintenanceJob.class);

MDC.clear();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -60,6 +61,8 @@ public void setup() {
ReflectionTestUtils.setField(job, "systemAttributeRepository", systemAttributeRepository);

addAppender(GeneratedKeystoreCleanupJob.class);

MDC.clear();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -53,6 +54,8 @@ public void setup() {
ReflectionTestUtils.setField(job, "jobRepository", jobRepository);
ReflectionTestUtils.setField(job, "systemAttributeRepository", systemAttributeRepository);
addAppender(JobMaintenanceJob.class);

MDC.clear();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -63,6 +64,8 @@ public void setup() {
ReflectionTestUtils.setField(job, "systemAttributeRepository", systemAttributeRepository);

addAppender(LdapUserSyncJob.class);

MDC.clear();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Void> requestEntity = new HttpEntity<>(TestUtils.getHttpHeaders(jwt));

// act
String path = "/secure/job_execution";
ResponseEntity<Void> response = executeHttpGet(path + url, requestEntity, Void.class);

// assert
assertNotNull(response);
assertEquals(expectedStatus, response.getStatusCode());
}
}
Loading

0 comments on commit a00da66

Please sign in to comment.