Skip to content

Commit

Permalink
Feature 3789 user can cancel job (#3799)
Browse files Browse the repository at this point in the history
* Added REST API for user cancel job #3789

* Applied spotless #3789

* Added 404 to openapi and replaced notauthorized by notfound #3789

* Added change requests #3789

* Added 204 to restDoc test #3789

* Added new management URL #3789

* Handle nullpointer #3789

* Refactored exception #3789

* Changed API URL #3789
  • Loading branch information
lorriborri committed Jan 17, 2025
1 parent a70efe5 commit 6b8f419
Show file tree
Hide file tree
Showing 13 changed files with 1,134 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.domain.administration;

import static com.mercedesbenz.sechub.sharedkernel.security.APIConstants.API_ADMINISTRATION;
import static com.mercedesbenz.sechub.sharedkernel.security.APIConstants.API_ANONYMOUS;
import static com.mercedesbenz.sechub.sharedkernel.security.APIConstants.*;

public class AdministrationAPIConstants {

Expand Down Expand Up @@ -126,4 +125,8 @@ private AdministrationAPIConstants() {
public static final String API_FETCH_NEW_API_TOKEN_BY_ONE_WAY_TOKEN = API_ANONYMOUS + "apitoken";
public static final String API_REQUEST_NEW_APITOKEN = API_ANONYMOUS + "refresh/apitoken/{emailAddress}";

/* +-----------------------------------------------------------------------+ */
/* +............................... Jobs ..................................+ */
/* +-----------------------------------------------------------------------+ */
public static final String API_USER_CANCEL_JOB = API_MANAGEMENT + "jobs/{jobUUID}/cancel";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
package com.mercedesbenz.sechub.domain.administration.job;

import java.util.Optional;
import java.util.Set;
import java.util.UUID;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import com.mercedesbenz.sechub.domain.administration.project.Project;
import com.mercedesbenz.sechub.domain.administration.user.User;
import com.mercedesbenz.sechub.domain.administration.user.UserRepository;
import com.mercedesbenz.sechub.sharedkernel.Step;
import com.mercedesbenz.sechub.sharedkernel.error.InternalServerErrorException;
import com.mercedesbenz.sechub.sharedkernel.error.NotFoundException;
import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService;
import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessage;
import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageFactory;
Expand All @@ -22,47 +24,82 @@
import com.mercedesbenz.sechub.sharedkernel.messaging.MessageDataKeys;
import com.mercedesbenz.sechub.sharedkernel.messaging.MessageID;
import com.mercedesbenz.sechub.sharedkernel.usecases.job.UseCaseAdminCancelsJob;
import com.mercedesbenz.sechub.sharedkernel.usecases.job.UseCaseUserCancelsJob;
import com.mercedesbenz.sechub.sharedkernel.validation.UserInputAssertion;

@Service
public class JobCancelService {

private static final Logger LOG = LoggerFactory.getLogger(JobCancelService.class);
private final AuditLogService auditLogService;
private final UserInputAssertion userInputAssertion;
private final DomainMessageService eventBusService;
private final JobInformationRepository jobInformationRepository;
private final UserRepository userRepository;

public JobCancelService(AuditLogService auditLogService, UserInputAssertion userInputAssertion, DomainMessageService eventBusService,
JobInformationRepository jobInformationRepository, UserRepository userRepository) {
this.auditLogService = auditLogService;
this.userInputAssertion = userInputAssertion;
this.eventBusService = eventBusService;
this.jobInformationRepository = jobInformationRepository;
this.userRepository = userRepository;
}

@Autowired
AuditLogService auditLogService;
@UseCaseAdminCancelsJob(@Step(number = 2, name = "Cancel job", description = "Will trigger event that job cancel requested"))
public void cancelJob(UUID jobUUID) {
userInputAssertion.assertIsValidJobUUID(jobUUID);

@Autowired
UserInputAssertion assertion;
auditLogService.log("Requested cancellation of job {}", jobUUID);

@Autowired
DomainMessageService eventBusService;
JobMessage message = buildMessage(jobUUID);

@Autowired
JobInformationRepository repository;
/* trigger event */
informCancelJobRequested(message);
}

@Autowired
UserRepository userRepository;
@UseCaseUserCancelsJob(@Step(number = 2, name = "Cancel job", description = "Will trigger event that job cancel requested"))
public void userCancelJob(UUID jobUUID, String userId) {
userInputAssertion.assertIsValidJobUUID(jobUUID);
userInputAssertion.assertIsValidUserId(userId);

@Validated
@UseCaseAdminCancelsJob(@Step(number = 2, name = "Cancel job", description = "Will trigger event that job cancel requested"))
public void cancelJob(UUID jobUUID) {
assertion.assertIsValidJobUUID(jobUUID);

auditLogService.log("Requested cancellation of job {}", jobUUID);
auditLogService.log("User {} requested cancellation of job {}", userId, jobUUID);
assertUserAllowedCancelJob(jobUUID, userId);

JobMessage message = buildMessage(jobUUID);

/* trigger event */
informCancelJobRequested(message);
}

private void assertUserAllowedCancelJob(UUID jobUUID, String userId) {
JobInformation jobInfo = jobInformationRepository.findById(jobUUID).orElseThrow(() -> {
LOG.debug("Job not found: {}", jobUUID);
return new NotFoundException("Job not found: " + jobUUID);
});

User user = userRepository.findOrFailUser(userId);
Set<Project> projects = user.getProjects();

if (projects == null) {
LOG.debug("Projects for user {} are null.", userId);
throw new InternalServerErrorException("Projects fore user %s are null.".formatted(userId));
}

boolean isForbidden = projects.stream().noneMatch(project -> project.getId().equals(jobInfo.getProjectId()));

if (isForbidden) {
LOG.debug("User {} is not allowed to cancel job {}", userId, jobUUID);
throw new NotFoundException("Job not found: " + jobUUID);
}
}

private JobMessage buildMessage(UUID jobUUID) {
JobMessage message = new JobMessage();

message.setJobUUID(jobUUID);

Optional<JobInformation> optJobInfo = repository.findById(jobUUID);
Optional<JobInformation> optJobInfo = jobInformationRepository.findById(jobUUID);
if (optJobInfo.isEmpty()) {
LOG.warn("Did not found job information, so not able to resolve owner email address");
return message;
Expand Down Expand Up @@ -91,5 +128,4 @@ private void informCancelJobRequested(JobMessage message) {

eventBusService.sendAsynchron(infoRequest);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.domain.administration.job;

import java.util.UUID;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;

import com.mercedesbenz.sechub.domain.administration.AdministrationAPIConstants;
import com.mercedesbenz.sechub.sharedkernel.Step;
import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants;
import com.mercedesbenz.sechub.sharedkernel.security.UserContextService;
import com.mercedesbenz.sechub.sharedkernel.usecases.job.UseCaseUserCancelsJob;

import jakarta.annotation.security.RolesAllowed;

@RestController
public class JobRestController {

private final UserContextService userContextService;
private final JobCancelService jobCancelService;

public JobRestController(UserContextService userContextService, JobCancelService jobCancelService) {
this.userContextService = userContextService;
this.jobCancelService = jobCancelService;
}

@UseCaseUserCancelsJob(@Step(number = 1, name = "Rest call", description = "Triggers job cancellation request, owners of project will be informed", needsRestDoc = true))
@PostMapping(path = AdministrationAPIConstants.API_USER_CANCEL_JOB, produces = { MediaType.APPLICATION_JSON_VALUE })
@RolesAllowed({ RoleConstants.ROLE_USER, RoleConstants.ROLE_SUPERADMIN, RoleConstants.ROLE_OWNER })
@ResponseStatus(HttpStatus.NO_CONTENT)
public void userCancelJob(@PathVariable(name = "jobUUID") UUID jobUUID) {
String userId = userContextService.getUserId();
jobCancelService.userCancelJob(jobUUID, userId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.domain.administration.job;

import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.*;

import java.util.Optional;
import java.util.Set;
import java.util.UUID;

import org.junit.Test;

import com.mercedesbenz.sechub.domain.administration.project.Project;
import com.mercedesbenz.sechub.domain.administration.user.User;
import com.mercedesbenz.sechub.domain.administration.user.UserRepository;
import com.mercedesbenz.sechub.sharedkernel.error.NotFoundException;
import com.mercedesbenz.sechub.sharedkernel.logging.AuditLogService;
import com.mercedesbenz.sechub.sharedkernel.messaging.DomainMessageService;
import com.mercedesbenz.sechub.sharedkernel.validation.UserInputAssertion;

public class JobCancelServiceTest {

private static final AuditLogService auditLogService = mock();
private static final UserInputAssertion userInputAssertion = mock();
private static final DomainMessageService eventBusService = mock();
private static final JobInformationRepository jobInformationRepository = mock();
private static final UserRepository userRepository = mock();
private static final JobCancelService serviceToTest = new JobCancelService(auditLogService, userInputAssertion, eventBusService, jobInformationRepository,
userRepository);

@Test
public void userCancelJob_receives_not_found_exception_when_job_not_found() {
/* prepare */
UUID jobUUID = UUID.randomUUID();
String userId = "user1";
when(jobInformationRepository.findById(jobUUID)).thenReturn(Optional.empty());

/* execute + test */
assertThatThrownBy(() -> serviceToTest.userCancelJob(jobUUID, userId)).isInstanceOf(NotFoundException.class);
verify(eventBusService, never()).sendAsynchron(any());

}

@Test
public void userCancelJob_receives_not_found_exception_when_project_not_assigned() {
/* prepare */
UUID jobUUID = UUID.randomUUID();
String userId = "user1";
String projectId = "project1";
String otherProjectId = "project2";

JobInformation jobInformation = mock(JobInformation.class);
when(jobInformation.getProjectId()).thenReturn(projectId);
Project project = mock(Project.class);
when(project.getId()).thenReturn(otherProjectId);

User user = mock(User.class);
when(user.getProjects()).thenReturn(Set.of(project));

when(userRepository.findOrFailUser(userId)).thenReturn(user);
when(jobInformationRepository.findById(jobUUID)).thenReturn(Optional.of(jobInformation));

/* execute + test */
assertThatThrownBy(() -> serviceToTest.userCancelJob(jobUUID, userId)).isInstanceOf(NotFoundException.class);
verify(eventBusService, never()).sendAsynchron(any());
}

@Test
public void userCancelJob_receives_no_exception_when_job_found_and_authorized() {
/* prepare */
UUID jobUUID = UUID.randomUUID();
String userId = "user1";
String projectId = "project1";

JobInformation jobInformation = mock(JobInformation.class);
when(jobInformation.getProjectId()).thenReturn(projectId);
Project project = mock(Project.class);
when(project.getId()).thenReturn(projectId);

User user = mock(User.class);
when(user.getProjects()).thenReturn(Set.of(project));

when(userRepository.findOrFailUser(userId)).thenReturn(user);
when(jobInformationRepository.findById(jobUUID)).thenReturn(Optional.of(jobInformation));

/* execute + test */
assertThatCode(() -> serviceToTest.userCancelJob(jobUUID, userId)).doesNotThrowAnyException();
verify(eventBusService, times(1)).sendAsynchron(any());
}

}
Loading

0 comments on commit 6b8f419

Please sign in to comment.