diff --git a/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts b/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts index 630137b..194b9a4 100644 --- a/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts +++ b/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts @@ -5,7 +5,7 @@ import * as FA from '@fortawesome/free-solid-svg-icons'; import {NbGetters, NbTreeGridDataSource, NbTreeGridDataSourceBuilder} from '@nebular/theme'; import {NotificationService, PopupType} from '@shared/services/notification.service'; import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; -import {filter, mergeMap, tap} from 'rxjs/operators'; +import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators'; import { Comment, CommentDialogBody, @@ -142,7 +142,33 @@ export class PentestCommentsComponent implements OnInit { } onClickDeleteComment(commentEntry): void { - console.info('Coming soon..', commentEntry); + const message = { + title: 'comment.delete.title', + key: 'comment.delete.key', + data: {name: commentEntry.data.title}, + }; + this.dialogService.openConfirmDialog( + message + ).onClose.pipe( + filter((confirm) => !!confirm), + switchMap(() => this.commentService.deleteCommentByPentestAndCommentId( + this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', + commentEntry.data.commentId) + ), + catchError(() => { + this.notificationService.showPopup('comment.popup.delete.failed', PopupType.FAILURE); + return []; + }), + untilDestroyed(this) + ).subscribe({ + next: (deletedComment: any) => { + this.store.dispatch(new UpdatePentestComments(deletedComment.id)); + this.loadCommentsData(); + this.notificationService.showPopup('comment.popup.delete.success', PopupType.SUCCESS); + }, error: error => { + console.error(error); + } + }); } // HTML only diff --git a/security-c4po-angular/src/shared/services/comment.service.ts b/security-c4po-angular/src/shared/services/comment.service.ts index 136c32f..dbd888e 100644 --- a/security-c4po-angular/src/shared/services/comment.service.ts +++ b/security-c4po-angular/src/shared/services/comment.service.ts @@ -34,4 +34,13 @@ export class CommentService { public saveComment(pentestId: string, comment: Comment): Observable { return this.http.post(`${this.apiBaseURL}/${pentestId}/comment`, comment); } + + /** + * Delete Comment + * @param pentestId the id of the pentest + * @param commentId the id of the comment + */ + public deleteCommentByPentestAndCommentId(pentestId: string, commentId: string): Observable { + return this.http.delete(`${this.apiBaseURL}/${pentestId}/comment/${commentId}`); + } } diff --git a/security-c4po-api/security-c4po-api.postman_collection.json b/security-c4po-api/security-c4po-api.postman_collection.json index 672abf8..92bb33c 100644 --- a/security-c4po-api/security-c4po-api.postman_collection.json +++ b/security-c4po-api/security-c4po-api.postman_collection.json @@ -538,6 +538,42 @@ } }, "response": [] + }, + { + "name": "deleteComment", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzE3OTg0OTQsImlhdCI6MTY3MTc5ODE5NCwianRpIjoiYjU0MTcxOGEtNjgxZC00ODI1LThkMGMtYzU5YWQ5Y2MzOGIyIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI0NDFhZWU3Ny1iMzNkLTQ0ZDMtOTliYS04ODc2NWU5ZGM5OGUiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.23KYKKUgjPiXWNR0po_VzU8BCwdN2KFZSIr5EdbKkpnQyJZWp4ov-mP8bVVsc_kfO8BBLudbWvZE6NLS9rSuBVi25kA5soZqDOhacJfAXBomEpcModDCFxm4917QtZCMFsBx571cMipQMc7Oo5WAtSDwoVsi3ju_WbVVq4si40zI-B0qmhRCg6SGGqkxu0oQr_aZ9JUIHM8mT1YQSV6jBmTkFLpQBcY6ren2JCjL2sceXIcUaWd5bqVeGEdo3gbsIQMxFiOzH0UeHKJg0Zyw9n4TEl9jyv6ncrgOpJ41Xw_1jtmcjh16uCZrzyiTaFIG7hCEGRi2O8gezQ2ClMTlnw", + "type": "string" + }, + { + "key": "undefined", + "type": "any" + } + ] + }, + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:8443/pentests/11601f51-bc17-47fd-847d-0c53df5405b5/comment/89703b19-16c7-49e5-8e33-0c706313e5fe", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8443", + "path": [ + "pentests", + "11601f51-bc17-47fd-847d-0c53df5405b5", + "comment", + "89703b19-16c7-49e5-8e33-0c706313e5fe" + ] + } + }, + "response": [] } ] }, diff --git a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc index 0c685de..a959d4a 100644 --- a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc +++ b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc @@ -237,7 +237,7 @@ include::{snippets}/updateFindingById/response-fields.adoc[] === Delete finding -To delete a finding, call the PATCH request /pentests/+{pentestId}+/finding/+{findingId}+ +To delete a finding, call the DELETE request /pentests/+{pentestId}+/finding/+{findingId}+ ==== Request example @@ -297,10 +297,32 @@ include::{snippets}/saveCommentByPentestId/http-response.adoc[] include::{snippets}/saveCommentByPentestId/response-fields.adoc[] +=== Delete comment + +To delete a comment, call the DELETE request /pentests/+{pentestId}+/comment/+{commentId}+ + +==== Request example + +include::{snippets}/deleteCommentByPentestAndCommentId/http-request.adoc[] + +==== Request structure + +include::{snippets}/deleteCommentByPentestAndCommentId/path-parameters.adoc[] + +==== Response example + +include::{snippets}/deleteCommentByPentestAndCommentId/http-response.adoc[] + +==== Response structure + +include::{snippets}/deleteCommentByPentestAndCommentId/response-fields.adoc[] + == Change History |=== |Date |Change +|2022-12-23 +|Added DELETE endpoint for Comment |2022-12-22 |Added GET, POST endpoint for Comment |2022-12-09 diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestService.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestService.kt index 3c41ea6..4915ad7 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestService.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/PentestService.kt @@ -247,4 +247,38 @@ class PentestService(private val pentestRepository: PentestRepository, private v throw ex }.map { pentestEntity -> pentestEntity.data.commentIds } } + + /** + * Update [Pentest] for Comment + * + * @throws [InvalidModelException] if the [Pentest] is invalid + * @throws [TransactionInterruptedException] if the [Pentest] could not be updated + * @return updated [Pentest] + */ + fun removeCommentFromPentest(pentestId: String, commentId: String): Mono { + return pentestRepository.findPentestById(pentestId).switchIfEmpty { + logger.warn("Pentest with id $pentestId not found. Updating not possible.") + val msg = "Pentest with id $pentestId not found." + val ex = EntityNotFoundException(msg, Errorcode.PentestNotFound) + throw ex + }.flatMap { currentPentestEntity: PentestEntity -> + if (currentPentestEntity.data.commentIds.find { pentestData -> pentestData == commentId } != null) { + val commentIds = currentPentestEntity.data.commentIds.toMutableList() + commentIds.remove(commentId) + currentPentestEntity.data.commentIds = commentIds.toList() + } + currentPentestEntity.lastModified = Instant.now() + this.pentestRepository.save(currentPentestEntity).map { + it.toPentest() + }.doOnError { + throw wrappedException( + logging = { logger.warn("Pentest could not be updated in Database. Thrown exception: ", it) }, + mappedException = TransactionInterruptedException( + "Pentest could not be updated.", + Errorcode.PentestInsertionFailed + ) + ) + } + } + } } \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentController.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentController.kt index 218ec22..628f7f3 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentController.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentController.kt @@ -8,6 +8,7 @@ import com.securityc4po.api.configuration.error.handler.EntityNotFoundException import com.securityc4po.api.configuration.error.handler.Errorcode import com.securityc4po.api.pentest.finding.toFindingResponseBody import com.securityc4po.api.pentest.PentestService +import com.securityc4po.api.pentest.finding.toFindingDeleteResponseBody import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity.noContent import org.springframework.web.bind.annotation.* @@ -48,4 +49,16 @@ class CommentController(private val pentestService: PentestService, private val ResponseEntity.accepted().body(it.toCommentResponseBody()) } } + + @DeleteMapping("/{pentestId}/comment/{commentId}") + fun deleteFinding( + @PathVariable(value = "pentestId") pentestId: String, + @PathVariable(value = "commentId") commentId: String + ): Mono> { + return this.commentService.deleteCommentByPentestAndCommentId(pentestId, commentId).map { + ResponseEntity.ok().body(it.toCommentDeleteResponseBody()) + }.switchIfEmpty { + Mono.just(noContent().build()) + } + } } \ No newline at end of file diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentService.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentService.kt index f023426..513e945 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentService.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentService.kt @@ -20,11 +20,11 @@ class CommentService(private val commentRepository: CommentRepository, private v var logger = getLoggerFor() /** - * Save [Finding] + * Save [Comment] * - * @throws [InvalidModelException] if the [Finding] is invalid - * @throws [TransactionInterruptedException] if the [Finding] could not be stored - * @return saved [Finding] + * @throws [InvalidModelException] if the [Comment] is invalid + * @throws [TransactionInterruptedException] if the [Comment] could not be stored + * @return saved [Comment] */ fun saveComment(pentestId: String, body: CommentRequestBody): Mono { validate( @@ -37,7 +37,7 @@ class CommentService(private val commentRepository: CommentRepository, private v val comment = body.toComment() val commentEntity = CommentEntity(comment) return commentRepository.insert(commentEntity).flatMap { newCommentEntity: CommentEntity -> - val finding = newCommentEntity.toComment() + val comment = newCommentEntity.toComment() // After successfully saving finding add id to pentest pentestService.updatePentestComment(pentestId, comment.id).onErrorMap { TransactionInterruptedException( @@ -45,7 +45,7 @@ class CommentService(private val commentRepository: CommentRepository, private v Errorcode.PentestInsertionFailed ) }.map { - finding + comment } }.doOnError { throw wrappedException( @@ -73,4 +73,38 @@ class CommentService(private val commentRepository: CommentRepository, private v throw ex } } -} \ No newline at end of file + + /** + * Delete [Comment] + * + * @throws [TransactionInterruptedException] if the [Comment] could not be deleted + * @return deleted [Comment] + */ + fun deleteCommentByPentestAndCommentId(pentestId: String, commentId: String): Mono { + return commentRepository.findCommentById(commentId).switchIfEmpty { + logger.info("Comment with id $commentId not found. Deletion not necessary.") + Mono.empty() + }.flatMap { commentEntity: CommentEntity -> + val comment = commentEntity.toComment() + commentRepository.deleteCommentById(commentId).flatMap { + // After successfully deleting comment remove its id from pentest + pentestService.removeCommentFromPentest(pentestId, commentId).onErrorMap { + TransactionInterruptedException( + "Pentest could not be updated in Database.", + Errorcode.PentestInsertionFailed + ) + }.map { + comment + } + }.doOnError { + throw wrappedException( + logging = { logger.warn("Comment could not be deleted from Database. Thrown exception: ", it) }, + mappedException = TransactionInterruptedException( + "Comment could not be deleted.", + Errorcode.CommentDeletionFailed + ) + ) + } + } + } +} diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/finding/FindingService.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/finding/FindingService.kt index ee9a761..42be4a3 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/finding/FindingService.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/finding/FindingService.kt @@ -125,6 +125,12 @@ class FindingService(private val findingRepository: FindingRepository, private v } } + /** + * Delete [Finding] + * + * @throws [TransactionInterruptedException] if the [Finding] could not be deleted + * @return deleted [Finding] + */ fun deleteFindingByPentestAndFindingId(pentestId: String, findingId: String): Mono { return findingRepository.findFindingById(findingId).switchIfEmpty { logger.info("Finding with id $findingId not found. Deletion not necessary.") diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerDocumentationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerDocumentationTest.kt index 0778128..ee68d0b 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerDocumentationTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerDocumentationTest.kt @@ -10,10 +10,6 @@ import com.securityc4po.api.pentest.Pentest import com.securityc4po.api.pentest.PentestCategory import com.securityc4po.api.pentest.PentestEntity import com.securityc4po.api.pentest.PentestStatus -import com.securityc4po.api.pentest.comment.Comment -import com.securityc4po.api.pentest.comment.CommentEntity -import com.securityc4po.api.pentest.comment.CommentRequestBody -import com.securityc4po.api.pentest.comment.toCommentResponseBody import com.securityc4po.api.project.Project import com.securityc4po.api.project.ProjectEntity import edu.umd.cs.findbugs.annotations.SuppressFBWarnings @@ -153,6 +149,43 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() { ) } + @Nested + inner class DeleteComment { + @Test + fun deleteCommentByPentestAndCommentId() { + val pentestId = "43fbc63c-f624-11ec-b939-0242ac120002" + val commentId = "ab62d365-1b1d-4da1-89bc-5496616e220f" + webTestClient.delete() + .uri("/pentests/{pentestId}/comment/{commentId}", pentestId, commentId) + .header("Authorization", "Bearer $tokenAdmin") + .exchange() + .expectStatus().isOk + .expectHeader().doesNotExist("") + .expectBody() + .consumeWith( + WebTestClientRestDocumentation.document( + "{methodName}", + Preprocessors.preprocessRequest( + Preprocessors.prettyPrint(), + Preprocessors.modifyUris().removePort(), + Preprocessors.removeHeaders("Host", "Content-Length") + ), + Preprocessors.preprocessResponse( + Preprocessors.prettyPrint() + ), + RequestDocumentation.relaxedPathParameters( + RequestDocumentation.parameterWithName("pentestId").description("The id of the pentest you want to remove the comment from"), + RequestDocumentation.parameterWithName("commentId").description("The id of the comment you want to delete") + ), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING) + .description("The id of the comment you deleted") + ) + ) + ) + } + } + private fun persistBasicTestScenario() { // setup test data // Project diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerIntegrationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerIntegrationTest.kt index 36603ec..b56d150 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerIntegrationTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerIntegrationTest.kt @@ -9,10 +9,6 @@ import com.securityc4po.api.pentest.Pentest import com.securityc4po.api.pentest.PentestCategory import com.securityc4po.api.pentest.PentestEntity import com.securityc4po.api.pentest.PentestStatus -import com.securityc4po.api.pentest.comment.Comment -import com.securityc4po.api.pentest.comment.CommentEntity -import com.securityc4po.api.pentest.comment.CommentRequestBody -import com.securityc4po.api.pentest.comment.toCommentResponseBody import com.securityc4po.api.project.Project import com.securityc4po.api.project.ProjectEntity import edu.umd.cs.findbugs.annotations.SuppressFBWarnings @@ -114,6 +110,23 @@ class CommentControllerIntegrationTest: BaseIntTest() { ) } + @Nested + inner class DeleteComment { + @Test + fun `delete finding successfully`() { + val pentestId = "43fbc63c-f624-11ec-b939-0242ac120002" + val commentId = "ab62d365-1b1d-4da1-89bc-5496616e220f" + webTestClient.delete() + .uri("/pentests/{pentestId}/comment/{commentId}", pentestId, commentId) + .header("Authorization", "Bearer $tokenAdmin") + .exchange() + .expectStatus().isOk + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody() + .jsonPath("$.id").isEqualTo("ab62d365-1b1d-4da1-89bc-5496616e220f") + } + } + private fun persistBasicTestScenario() { // setup test data // project diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/finding/FindingControllerDocumentationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/finding/FindingControllerDocumentationTest.kt index 5caaa32..056bd36 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/finding/FindingControllerDocumentationTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/finding/FindingControllerDocumentationTest.kt @@ -318,7 +318,7 @@ class FindingControllerDocumentationTest: BaseDocumentationIntTest() { Preprocessors.prettyPrint() ), RequestDocumentation.relaxedPathParameters( - RequestDocumentation.parameterWithName("pentestId").description("The id of the pentest you want to remove the finidng from"), + RequestDocumentation.parameterWithName("pentestId").description("The id of the pentest you want to remove the finding from"), RequestDocumentation.parameterWithName("findingId").description("The id of the finding you want to delete") ), PayloadDocumentation.relaxedResponseFields(