diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/AdministrationAPIConstants.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/AdministrationAPIConstants.java index aa31b4545c..ba3cdcaf21 100644 --- a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/AdministrationAPIConstants.java +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/AdministrationAPIConstants.java @@ -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 { @@ -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"; } diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobCancelService.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobCancelService.java index be8e68616f..d988257b0c 100644 --- a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobCancelService.java +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobCancelService.java @@ -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; @@ -22,34 +24,47 @@ 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); @@ -57,12 +72,34 @@ public void cancelJob(UUID jobUUID) { 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 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 optJobInfo = repository.findById(jobUUID); + Optional optJobInfo = jobInformationRepository.findById(jobUUID); if (optJobInfo.isEmpty()) { LOG.warn("Did not found job information, so not able to resolve owner email address"); return message; @@ -91,5 +128,4 @@ private void informCancelJobRequested(JobMessage message) { eventBusService.sendAsynchron(infoRequest); } - } diff --git a/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobRestController.java b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobRestController.java new file mode 100644 index 0000000000..8f02cc12a3 --- /dev/null +++ b/sechub-administration/src/main/java/com/mercedesbenz/sechub/domain/administration/job/JobRestController.java @@ -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); + } +} diff --git a/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/job/JobCancelServiceTest.java b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/job/JobCancelServiceTest.java new file mode 100644 index 0000000000..1f7530e5fe --- /dev/null +++ b/sechub-administration/src/test/java/com/mercedesbenz/sechub/domain/administration/job/JobCancelServiceTest.java @@ -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()); + } + +} diff --git a/sechub-api-java/src/main/resources/reduced-openapi3.json b/sechub-api-java/src/main/resources/reduced-openapi3.json index d44eb27cf5..22e374d2e8 100644 --- a/sechub-api-java/src/main/resources/reduced-openapi3.json +++ b/sechub-api-java/src/main/resources/reduced-openapi3.json @@ -307,7 +307,7 @@ "properties": { "projectData": { "type": "array", - "description": "Porject data list containing false positive setup for the project", + "description": "Project data list containing false positive setup for the project", "items": { "type": "object", "properties": { @@ -648,7 +648,7 @@ "properties": { "key1": { "type": "string", - "description": "An arbitrary metadata key." + "description": "An arbitrary metadata key" } }, "description": "An JSON object containing metadata key-value pairs defined for this project." @@ -657,6 +657,26 @@ "type": "string", "description": "The project access level" }, + "templateIds": { + "type": "array", + "description": "A list of all templates assigned to the project", + "items": { + "oneOf": [ + { + "type": "object" + }, + { + "type": "boolean" + }, + { + "type": "string" + }, + { + "type": "number" + } + ] + } + }, "description": { "type": "string", "description": "The project description." @@ -670,11 +690,11 @@ }, "projectId": { "type": "string", - "description": "The name of the project." + "description": "The name of the project" }, "users": { "type": "array", - "description": "A list of all users having access to the project.", + "description": "A list of all users having access to the project", "items": { "type": "string" } @@ -1074,6 +1094,32 @@ "login": { "type": "object", "properties": { + "totp": { + "type": "object", + "properties": { + "seed": { + "type": "string", + "description": "The seed/secret for the TOTP generation. If TOTP is configured this parameter is mandatory." + }, + "tokenLength": { + "type": "number", + "description": "The length of the generated TOTP. In most cases nothing is specified and the default length '6' is used." + }, + "encodingType": { + "type": "string", + "description": "The encoding type of the 'seed'. The default value is 'AUTODETECT'. Currently available values are: 'BASE64', 'BASE32', 'HEX', 'PLAIN', 'AUTODETECT'" + }, + "hashAlgorithm": { + "type": "string", + "description": "The hash algorithm to generate the TOTP. In most cases nothing is specified and the default hash algorithm 'HMAC_SHA1' is used. Currently available values are: 'HMAC_SHA1', 'HMAC_SHA256', 'HMAC_SHA512'" + }, + "validityInSeconds": { + "type": "number", + "description": "The time in seconds the generated TOTP is valid. In most cases nothing is specified and the default of '30' seconds is used." + } + }, + "description": "Optional TOTP configuration as an additional authentication factor." + }, "form": { "type": "object", "properties": { @@ -1183,6 +1229,54 @@ } } }, + "Templates": { + "title": "Templates", + "type": "object", + "properties": { + "variables": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The variable name" + }, + "optional": { + "type": "boolean", + "description": "Defines if the variable is optional. The default is false" + }, + "validation": { + "type": "object", + "properties": { + "regularExpression": { + "type": "string", + "description": "A regular expression which must match to accept the user input inside the variable" + }, + "minLength": { + "type": "number", + "description": "The minimum content length of this variable" + }, + "maxLength": { + "type": "number", + "description": "The maximum content length of this variable" + } + }, + "description": "Defines a simple validation segment." + } + } + } + }, + "assetId": { + "type": "string", + "description": "The asset id used by the template" + }, + "type": { + "type": "string", + "description": "The template type. Must be be defined when a new template is created. An update will ignore changes of this property because the type is immutable! Currently supported types are: [Lcom.mercedesbenz.sechub.commons.model.template.TemplateType;@24ae091b" + } + } + }, "UserDetails": { "title": "UserDetails", "type": "object", @@ -1233,8 +1327,165 @@ } } }, + "api-admin-asset-assetId-details924562158": { + "type": "object", + "properties": { + "assetId": { + "type": "string", + "description": "The asset identifier" + }, + "files": { + "type": "array", + "description": "Array containing data about files from asset", + "items": { + "type": "object", + "properties": { + "fileName": { + "type": "string", + "description": "Name of file" + }, + "checksum": { + "type": "string", + "description": "Checksum for file" + } + } + } + } + } + }, + "api-admin-asset-ids519982223": { + "type": "array", + "description": "Array contains all existing asset identifiers", + "items": { + "oneOf": [ + { + "type": "object" + }, + { + "type": "boolean" + }, + { + "type": "string" + }, + { + "type": "number" + } + ] + } + }, "api-admin-encryption-rotate-638361826": { "type": "object" + }, + "api-admin-template-templateId1476369005": { + "type": "object", + "properties": { + "variables": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The variable name" + }, + "optional": { + "type": "boolean", + "description": "Defines if the variable is optional. The default is false" + }, + "validation": { + "type": "object", + "properties": { + "regularExpression": { + "type": "string", + "description": "A regular expression which must match to accept the user input inside the variable" + }, + "minLength": { + "type": "number", + "description": "The minimum content length of this variable" + }, + "maxLength": { + "type": "number", + "description": "The maximum content length of this variable" + } + }, + "description": "Defines a simple validation segment." + } + } + } + }, + "assetId": { + "type": "string", + "description": "The asset id used by the template" + }, + "id": { + "type": "string", + "description": "The (unique) template id" + }, + "type": { + "type": "string", + "description": "The template type. Currently supported types are: [Lcom.mercedesbenz.sechub.commons.model.template.TemplateType;@5372b370" + } + } + }, + "api-admin-templates-753726831": { + "type": "array", + "description": "Array contains all existing template identifiers", + "items": { + "oneOf": [ + { + "type": "object" + }, + { + "type": "boolean" + }, + { + "type": "string" + }, + { + "type": "number" + } + ] + } + }, + "api-projects-387937430": { + "type": "array", + "items": { + "type": "object", + "properties": { + "owner": { + "type": "string", + "description": "Name of owner of the Project" + }, + "isOwned": { + "type": "boolean", + "description": "If caller is owner of the project" + }, + "assignedUsers": { + "type": "array", + "description": "Optional: Assigned users (only viewable by owner or admin)", + "items": { + "oneOf": [ + { + "type": "object" + }, + { + "type": "boolean" + }, + { + "type": "string" + }, + { + "type": "number" + } + ] + } + }, + "projectId": { + "type": "string", + "description": "Project ID" + } + } + } } }, "securitySchemes": { @@ -1280,6 +1531,225 @@ ] } }, + "/api/admin/asset/ids": { + "get": { + "tags": [ + "admin" + ], + "summary": "Admin fetches asset ids", + "description": "An administrator fetches all available asset ids.", + "operationId": "adminFetchAssetIds", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api-admin-asset-ids519982223" + } + } + } + } + }, + "security": [ + { + "basic": [ + + ] + } + ] + } + }, + "/api/admin/asset/{assetId}": { + "delete": { + "tags": [ + "admin" + ], + "summary": "Admin deletes asset comletely", + "description": "An administrator deletes an asset completely.", + "operationId": "adminDeletesAssetCompletely", + "parameters": [ + { + "name": "assetId", + "in": "path", + "description": "The asset identifier for the asset which shall be deleted completely", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "200" + } + }, + "security": [ + { + "basic": [ + + ] + } + ] + } + }, + "/api/admin/asset/{assetId}/details": { + "get": { + "tags": [ + "admin" + ], + "summary": "Admin fetches asset details", + "description": "An administrator fetches details about an asset. For example: the result will contain names but also checksum of files.", + "operationId": "adminFetchAssetDetails", + "parameters": [ + { + "name": "assetId", + "in": "path", + "description": "", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api-admin-asset-assetId-details924562158" + } + } + } + } + }, + "security": [ + { + "basic": [ + + ] + } + ] + } + }, + "/api/admin/asset/{assetId}/file": { + "post": { + "tags": [ + "admin" + ], + "summary": "Admin uploads an asset file", + "description": "An administrator uploads a file for an asset. If the file already exists, it will be overriden.", + "operationId": "adminUploadsAssetFile", + "parameters": [ + { + "name": "assetId", + "in": "path", + "description": "The id of the asset to which the uploaded file belongs to", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "checkSum", + "in": "query", + "description": "A sha256 checksum for file upload validation", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "200" + } + } + } + }, + "/api/admin/asset/{assetId}/file/{fileName}": { + "get": { + "tags": [ + "admin" + ], + "summary": "Admin downloads an asset file", + "description": "An administrator downloads a file fom an asset.", + "operationId": "adminDownloadsAssetFile", + "parameters": [ + { + "name": "assetId", + "in": "path", + "description": "The asset identifier", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fileName", + "in": "path", + "description": "The name of the file to download from asset", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "200" + } + }, + "security": [ + { + "basic": [ + + ] + } + ] + }, + "delete": { + "tags": [ + "admin" + ], + "summary": "Admin deletes an asset file", + "description": "An administrator deletes a file fom an asset.", + "operationId": "adminDeletesAssetFile", + "parameters": [ + { + "name": "assetId", + "in": "path", + "description": "The asset identifier", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "fileName", + "in": "path", + "description": "The name of the file to delete inside the asset", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "200" + } + }, + "security": [ + { + "basic": [ + + ] + } + ] + } + }, "/api/admin/config/autoclean": { "get": { "tags": [ @@ -2118,7 +2588,7 @@ { "name": "projectId", "in": "path", - "description": "The id for project to show details for", + "description": "The project id to show details for", "required": true, "schema": { "type": "string" @@ -2156,7 +2626,7 @@ { "name": "projectId", "in": "path", - "description": "The id for project to change details for", + "description": "The project id to change details for", "required": true, "schema": { "type": "string" @@ -2203,7 +2673,7 @@ { "name": "projectId", "in": "path", - "description": "The id for project to delete", + "description": "The project id to delete", "required": true, "schema": { "type": "string" @@ -2236,7 +2706,7 @@ { "name": "projectId", "in": "path", - "description": "The id for project", + "description": "The project id", "required": true, "schema": { "type": "string" @@ -2278,7 +2748,7 @@ { "name": "projectId", "in": "path", - "description": "The id for project", + "description": "The project id", "required": true, "schema": { "type": "string" @@ -2318,7 +2788,7 @@ { "name": "projectId", "in": "path", - "description": "The id for project", + "description": "The project id", "required": true, "schema": { "type": "string" @@ -2402,7 +2872,7 @@ { "name": "projectId", "in": "path", - "description": "The id for project", + "description": "The project id", "required": true, "schema": { "type": "string" @@ -2472,6 +2942,88 @@ ] } }, + "/api/admin/project/{projectId}/template/{templateId}": { + "put": { + "tags": [ + "admin" + ], + "summary": "Admin assigns template to project", + "description": "An administrator assigns a template to a project", + "operationId": "adminAssignTemplateToProject", + "parameters": [ + { + "name": "projectId", + "in": "path", + "description": "The project id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "templateId", + "in": "path", + "description": "The id of the template to assign to project", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "200" + } + }, + "security": [ + { + "basic": [ + + ] + } + ] + }, + "delete": { + "tags": [ + "admin" + ], + "summary": "Admin unassigns template from project", + "description": "An administrator unassigns a template from a project", + "operationId": "adminUnassignTemplateFromProject", + "parameters": [ + { + "name": "projectId", + "in": "path", + "description": "The project id", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "templateId", + "in": "path", + "description": "The id of the template to unassign from project", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "200" + } + }, + "security": [ + { + "basic": [ + + ] + } + ] + } + }, "/api/admin/project/{projectId}/whitelist": { "post": { "tags": [ @@ -2773,6 +3325,146 @@ ] } }, + "/api/admin/template/{templateId}": { + "get": { + "tags": [ + "admin" + ], + "summary": "Admin fetches template", + "description": "An administrator fetches template data by its id", + "operationId": "adminFetchTemplate", + "parameters": [ + { + "name": "templateId", + "in": "path", + "description": "The (unique) template id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api-admin-template-templateId1476369005" + } + } + } + } + }, + "security": [ + { + "basic": [ + + ] + } + ] + }, + "put": { + "tags": [ + "admin" + ], + "summary": "Admin creates or updates a template", + "description": "An administrator creates or updates a template", + "operationId": "adminCreateOrUpdateTemplate", + "parameters": [ + { + "name": "templateId", + "in": "path", + "description": "The (unique) template id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json;charset=UTF-8": { + "schema": { + "$ref": "#/components/schemas/Templates" + } + } + } + }, + "responses": { + "200": { + "description": "200" + } + }, + "security": [ + { + "basic": [ + + ] + } + ] + }, + "delete": { + "tags": [ + "admin" + ], + "summary": "Admin deletes a template", + "description": "An administrator deletes a template. Will also remove any association between projects and this template", + "operationId": "adminDeleteTemplate", + "parameters": [ + { + "name": "templateId", + "in": "path", + "description": "The (unique) template id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "200" + } + }, + "security": [ + { + "basic": [ + + ] + } + ] + } + }, + "/api/admin/templates": { + "get": { + "tags": [ + "admin" + ], + "summary": "Admin fetches all template ids", + "description": "An administrator fetches a list containing all templates ids", + "operationId": "adminFetchAllTemplateIds", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api-admin-templates-753726831" + } + } + } + } + }, + "security": [ + { + "basic": [ + + ] + } + ] + } + }, "/api/admin/user-by-email/{emailAddress}": { "get": { "tags": [ @@ -3134,6 +3826,39 @@ } } }, + "/api/management/jobs/cancel/{jobUUID}": { + "post": { + "tags": [ + "management" + ], + "summary": "User cancels a job", + "description": "User does cancel a job by its Job UUID", + "operationId": "userCancelsJob", + "parameters": [ + { + "name": "jobUUID", + "in": "path", + "description": "The job UUID", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "204" + } + }, + "security": [ + { + "basic": [ + + ] + } + ] + } + }, "/api/project/{projectId}/false-positive/project-data/{id}": { "delete": { "tags": [ @@ -3744,12 +4469,12 @@ "200": { "description": "200", "content": { - "application/json": { + "text/html;charset=UTF-8": { "schema": { "$ref": "#/components/schemas/SecHubReport" } }, - "text/html;charset=UTF-8": { + "application/json": { "schema": { "$ref": "#/components/schemas/SecHubReport" } @@ -3761,6 +4486,35 @@ { "basic": [ + ] + } + ] + } + }, + "/api/projects": { + "get": { + "tags": [ + "project" + ], + "summary": "get assigned project data", + "description": "get assigned project data", + "operationId": "getAssignedProjectDataList", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/api-projects-387937430" + } + } + } + } + }, + "security": [ + { + "basic": [ + ] } ] diff --git a/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/JobRestControllerRestDocTest.java b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/JobRestControllerRestDocTest.java new file mode 100644 index 0000000000..cb5d1bf982 --- /dev/null +++ b/sechub-doc/src/test/java/com/mercedesbenz/sechub/restdoc/JobRestControllerRestDocTest.java @@ -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 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().isNoContent()). + andDo(defineRestService(). + with(). + useCaseData(useCase). + tag(RestDocFactory.extractTag(apiEndpoint)). + and(). + document( + requestHeaders( + + ), + pathParameters( + parameterWithName(JOB_UUID.paramName()).description("The job UUID") + ) + )); + + /* @formatter:on */ + } +} diff --git a/sechub-openapi-java/src/main/resources/openapi.yaml b/sechub-openapi-java/src/main/resources/openapi.yaml index 28369cef85..69d55763da 100644 --- a/sechub-openapi-java/src/main/resources/openapi.yaml +++ b/sechub-openapi-java/src/main/resources/openapi.yaml @@ -34,6 +34,8 @@ tags: description: Operations relevant to encryption - name: Other description: All other use cases + - name: Job Management + description: Operations relevant to manage jobs components: securitySchemes: @@ -3862,7 +3864,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 @@ -4171,4 +4173,29 @@ paths: "404": description: "Not found" tags: - - Configuration \ No newline at end of file + - Configuration + + /api/management/jobs/{jobUUID}/cancel: + 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: + "204": + description: "No content" + "404": + description: "Not found" + security: + - basic: [ ] + summary: User cancels a job + tags: + - Job Management + x-accepts: application/json \ No newline at end of file diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/error/InternalServerErrorException.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/error/InternalServerErrorException.java new file mode 100644 index 0000000000..6307fa1b28 --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/error/InternalServerErrorException.java @@ -0,0 +1,17 @@ +package com.mercedesbenz.sechub.sharedkernel.error; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) +public class InternalServerErrorException extends RuntimeException { + private static final long serialVersionUID = 8392017456124983765L; + + public InternalServerErrorException() { + this("An internal server error occurred!"); + } + + public InternalServerErrorException(String message) { + super(message); + } +} diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/security/APIConstants.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/security/APIConstants.java index b23044cc48..9251c72e12 100644 --- a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/security/APIConstants.java +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/security/APIConstants.java @@ -55,6 +55,11 @@ private APIConstants() { */ public static final String API_PROJECTS = "/api/projects"; + /** + * API starting with this is accessible by users for project jobs + */ + public static final String API_MANAGEMENT = "/api/management/"; + /** * Actuator endpoints are available anonymous */ diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/security/SecHubSecurityConfiguration.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/security/SecHubSecurityConfiguration.java index 52ed67b53f..2f87b81ffe 100644 --- a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/security/SecHubSecurityConfiguration.java +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/security/SecHubSecurityConfiguration.java @@ -37,6 +37,7 @@ protected Customizer.Authorization .requestMatchers(APIConstants.API_PROJECT + "**").hasAnyRole(RoleConstants.ROLE_USER, RoleConstants.ROLE_SUPERADMIN) .requestMatchers(APIConstants.API_OWNER + "**").hasAnyRole(RoleConstants.ROLE_OWNER, RoleConstants.ROLE_SUPERADMIN) .requestMatchers(APIConstants.API_PROJECTS).hasAnyRole(RoleConstants.ROLE_USER, RoleConstants.ROLE_SUPERADMIN, RoleConstants.ROLE_OWNER) + .requestMatchers(APIConstants.API_MANAGEMENT + "**").hasAnyRole(RoleConstants.ROLE_USER, RoleConstants.ROLE_SUPERADMIN, RoleConstants.ROLE_OWNER) .requestMatchers(APIConstants.API_ANONYMOUS + "**").permitAll() .requestMatchers(APIConstants.ERROR_PAGE).permitAll() .requestMatchers(APIConstants.ACTUATOR + "**").permitAll() diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/UseCaseIdentifier.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/UseCaseIdentifier.java index a7e3dbdb3b..de79c6097d 100644 --- a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/UseCaseIdentifier.java +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/UseCaseIdentifier.java @@ -209,6 +209,8 @@ public enum UseCaseIdentifier { UC_ADMIN_DELETES_ASSET_COMPLETELY(93), + UC_USER_CANCELS_JOB(94), + ; /* +-----------------------------------------------------------------------+ */ /* +............................ Helpers ................................+ */ diff --git a/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/job/UseCaseUserCancelsJob.java b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/job/UseCaseUserCancelsJob.java new file mode 100644 index 0000000000..72e2a48c3d --- /dev/null +++ b/sechub-shared-kernel/src/main/java/com/mercedesbenz/sechub/sharedkernel/usecases/job/UseCaseUserCancelsJob.java @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +package com.mercedesbenz.sechub.sharedkernel.usecases.job; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import com.mercedesbenz.sechub.sharedkernel.Step; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseDefinition; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseGroup; +import com.mercedesbenz.sechub.sharedkernel.usecases.UseCaseIdentifier; + +/* @formatter:off */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@UseCaseDefinition( + id=UseCaseIdentifier.UC_USER_CANCELS_JOB, + group=UseCaseGroup.JOB_ADMINISTRATION, + apiName="userCancelsJob", + title="User cancels a job", + description="User does cancel a job by its Job UUID") +public @interface UseCaseUserCancelsJob { + Step value(); +} +/* @formatter:on */ diff --git a/sechub-testframework/src/main/java/com/mercedesbenz/sechub/test/SecHubTestURLBuilder.java b/sechub-testframework/src/main/java/com/mercedesbenz/sechub/test/SecHubTestURLBuilder.java index 10a5141df5..3af5f15500 100644 --- a/sechub-testframework/src/main/java/com/mercedesbenz/sechub/test/SecHubTestURLBuilder.java +++ b/sechub-testframework/src/main/java/com/mercedesbenz/sechub/test/SecHubTestURLBuilder.java @@ -26,6 +26,7 @@ public class SecHubTestURLBuilder extends AbstractTestURLBuilder { private static final String API_ADMIN_CONFIG_MAPPING = API_ADMIN_CONFIG + "/mapping"; private static final String API_PROJECT = "/api/project"; private static final String API_PROJECTS = "/api/projects"; + private static final String API_MANAGEMENT = "/api/management"; public static SecHubTestURLBuilder https(int port) { return new SecHubTestURLBuilder("https", port); @@ -121,6 +122,10 @@ private static ParameterBuilder params() { return new ParameterBuilder(); } + public String buildUserCancelJob(String jobUUID) { + return buildUrl(API_MANAGEMENT, "jobs/", jobUUID, "/cancel"); + } + private static class ParameterBuilder { private Map map = new LinkedHashMap<>();