Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature 3789 user can cancel job #3799

Merged
merged 9 commits into from
Jan 16, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 ................................+ */
lorriborri marked this conversation as resolved.
Show resolved Hide resolved
/* +-----------------------------------------------------------------------+ */
public static final String API_USER_CANCEL_JOB = API_JOBS + "cancel/{jobUUID}";
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@

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.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 +23,74 @@
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;

@Autowired
UserInputAssertion assertion;
@Validated
lorriborri marked this conversation as resolved.
Show resolved Hide resolved
@UseCaseAdminCancelsJob(@Step(number = 2, name = "Cancel job", description = "Will trigger event that job cancel requested"))
public void cancelJob(UUID jobUUID) {
userInputAssertion.assertIsValidJobUUID(jobUUID);

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

@Autowired
JobInformationRepository repository;
JobMessage message = buildMessage(jobUUID);

@Autowired
UserRepository userRepository;
/* trigger event */
informCancelJobRequested(message);
}

@Validated
@UseCaseAdminCancelsJob(@Step(number = 2, name = "Cancel job", description = "Will trigger event that job cancel requested"))
public void cancelJob(UUID jobUUID) {
assertion.assertIsValidJobUUID(jobUUID);
@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);

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) {
lorriborri marked this conversation as resolved.
Show resolved Hide resolved
JobInformation jobInfo = jobInformationRepository.findById(jobUUID).orElseThrow(() -> new NotFoundException("Job not found: " + jobUUID));

User user = userRepository.findOrFailUser(userId);
for (Project project : user.getProjects()) {
lorriborri marked this conversation as resolved.
Show resolved Hide resolved
lorriborri marked this conversation as resolved.
Show resolved Hide resolved
if (project.getId().equals(jobInfo.getProjectId())) {
return;
}
}
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 +119,4 @@ private void informCancelJobRequested(JobMessage message) {

eventBusService.sendAsynchron(infoRequest);
}

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

import java.util.UUID;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

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 {
hamidonos marked this conversation as resolved.
Show resolved Hide resolved

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))
@RequestMapping(path = AdministrationAPIConstants.API_USER_CANCEL_JOB, method = RequestMethod.POST, produces = { MediaType.APPLICATION_JSON_VALUE })
lorriborri marked this conversation as resolved.
Show resolved Hide resolved
@RolesAllowed({ RoleConstants.ROLE_USER, RoleConstants.ROLE_SUPERADMIN, RoleConstants.ROLE_OWNER })
public void userCancelJob(@PathVariable(name = "jobUUID") UUID jobUUID) {
lorriborri marked this conversation as resolved.
Show resolved Hide resolved
String userId = userContextService.getUserId();
jobCancelService.userCancelJob(jobUUID, userId);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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.mock;
import static org.mockito.Mockito.when;

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 {
lorriborri marked this conversation as resolved.
Show resolved Hide resolved

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);
}

@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);
}

@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();
}

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

import static com.mercedesbenz.sechub.restdoc.RestDocumentation.defineRestService;
import static com.mercedesbenz.sechub.test.RestDocPathParameter.JOB_UUID;
import static com.mercedesbenz.sechub.test.SecHubTestURLBuilder.https;
import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.lang.annotation.Annotation;
import java.util.UUID;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import com.mercedesbenz.sechub.docgen.util.RestDocFactory;
import com.mercedesbenz.sechub.domain.administration.job.JobCancelService;
import com.mercedesbenz.sechub.domain.administration.job.JobRestController;
import com.mercedesbenz.sechub.sharedkernel.Profiles;
import com.mercedesbenz.sechub.sharedkernel.security.RoleConstants;
import com.mercedesbenz.sechub.sharedkernel.security.UserContextService;
import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseRestDoc;
import com.mercedesbenz.sechub.sharedkernel.usecases.job.UseCaseUserCancelsJob;
import com.mercedesbenz.sechub.test.ExampleConstants;
import com.mercedesbenz.sechub.test.TestIsNecessaryForDocumentation;
import com.mercedesbenz.sechub.test.TestPortProvider;

@RunWith(SpringRunner.class)
@WebMvcTest
@ContextConfiguration(classes = { JobRestController.class })
@Import(TestRestDocSecurityConfiguration.class)
@WithMockUser(roles = RoleConstants.ROLE_USER)
@ActiveProfiles({ Profiles.TEST })
@AutoConfigureRestDocs(uriScheme = "https", uriHost = ExampleConstants.URI_SECHUB_SERVER, uriPort = 443)
public class JobRestControllerRestDocTest implements TestIsNecessaryForDocumentation {
private static final int PORT_USED = TestPortProvider.DEFAULT_INSTANCE.getRestDocTestPort();

@Autowired
private MockMvc mockMvc;

@MockBean
private JobCancelService jobCancelService;

@MockBean
private UserContextService userContextService;

@Test
@UseCaseRestDoc(useCase = UseCaseUserCancelsJob.class)
public void user_role_cancel_job() throws Exception {
/* prepare */
String apiEndpoint = https(PORT_USED).buildUserCancelJob(JOB_UUID.pathElement());
Class<? extends Annotation> useCase = UseCaseUserCancelsJob.class;

/* execute + test @formatter:off */
UUID jobUUID = UUID.randomUUID();
this.mockMvc.perform(
post(apiEndpoint, jobUUID).
contentType(MediaType.APPLICATION_JSON_VALUE).
header(AuthenticationHelper.HEADER_NAME, AuthenticationHelper.getHeaderValue())
).
andExpect(status().isOk()).
andDo(defineRestService().
with().
useCaseData(useCase).
tag(RestDocFactory.extractTag(apiEndpoint)).
and().
document(
requestHeaders(

),
pathParameters(
parameterWithName(JOB_UUID.paramName()).description("The job UUID")
)
));

/* @formatter:on */
}
}
29 changes: 27 additions & 2 deletions sechub-openapi-java/src/main/resources/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3862,7 +3862,7 @@ paths:
get:
summary: User lists jobs for project
description: User lists jobs for project
operationId: userListJobsForProject
operationId: userListsJobsForProject
parameters:
- name: projectId
description: The id of the project where job information shall be fetched
Expand Down Expand Up @@ -4171,4 +4171,29 @@ paths:
"404":
description: "Not found"
tags:
- Configuration
- Configuration

/api/jobs/cancel/{jobUUID}:
post:
description: User cancels a job by its Job UUID
operationId: userCancelsJob
parameters:
- description: The job UUID
explode: false
in: path
name: jobUUID
required: true
schema:
type: string
style: simple
responses:
"200":
description: "200"
"404":
description: "Not found"
security:
- basic: [ ]
summary: User cancels a job
tags:
- Job Administration
x-accepts: application/json
Loading
Loading