From 43604ed21235f2e6a98bc111610dfd26dc274ef3 Mon Sep 17 00:00:00 2001 From: Patrick Kuo Date: Mon, 22 Jan 2018 10:45:25 +0000 Subject: [PATCH] Handle rejected jira issues in doorman (#371) * handle reject status from jira - WIP * fix up after rebase * address PR issue and fix build error after rebase --- network-management/README.md | 5 +- network-management/build.gradle | 2 +- network-management/doorman.conf | 1 - .../CertificationRequestStorage.kt | 2 +- .../PersistentCertificateRequestStorage.kt | 2 +- .../doorman/DoormanParameters.kt | 3 +- .../corda/networkmanage/doorman/JiraCient.kt | 78 +++++++++++++++---- .../doorman/NetworkManagementServer.kt | 6 +- .../doorman/signer/CsrHandler.kt | 10 +-- .../doorman/signer/JiraCsrHandler.kt | 49 ++++++++---- .../webservice/NetworkMapWebService.kt | 1 - .../doorman/DoormanParametersTest.kt | 1 - .../networkmanage/doorman/JiraClientTest.kt | 55 +++++++++++++ .../doorman/signer/DefaultCsrHandlerTest.kt | 2 +- .../doorman/signer/JiraCsrHandlerTest.kt | 72 +++++++++++++++-- .../webservice/RegistrationWebServiceTest.kt | 8 +- 16 files changed, 231 insertions(+), 66 deletions(-) create mode 100644 network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/JiraClientTest.kt diff --git a/network-management/README.md b/network-management/README.md index 356c6c5348..11a60535d3 100644 --- a/network-management/README.md +++ b/network-management/README.md @@ -65,13 +65,15 @@ The doorman service can use JIRA to manage the certificate signing request appro projectCode = "TD" username = "username" password = "password" - doneTransitionCode = 41 } . . . } ``` +#### JIRA project configuration +* The JIRA project should setup as "Business Project" with "Task" workflow. +* Custom text field input "Request ID", and "Reject Reason" should be created in JIRA, doorman will exit with error without these custom fields. ### Auto approval When `approveAll` is set to `true`, the doorman will approve all requests on receive. (*This should only be enabled in a test environment) @@ -118,7 +120,6 @@ doormanConfig { projectCode = "TD" username = "username" password = "password" - doneTransitionCode = 41 } } diff --git a/network-management/build.gradle b/network-management/build.gradle index 40abcf0792..c022c203cc 100644 --- a/network-management/build.gradle +++ b/network-management/build.gradle @@ -88,7 +88,7 @@ dependencies { testCompile "com.nhaarman:mockito-kotlin:0.6.1" testCompile "com.spotify:docker-client:8.9.1" - compile('com.atlassian.jira:jira-rest-java-client-core:4.0.0') { + compile('com.atlassian.jira:jira-rest-java-client-core:5.0.4') { // The jira client includes jersey-core 1.5 which breaks everything. exclude module: 'jersey-core' } diff --git a/network-management/doorman.conf b/network-management/doorman.conf index e3a10650f8..ed58ea2d6f 100644 --- a/network-management/doorman.conf +++ b/network-management/doorman.conf @@ -23,7 +23,6 @@ doormanConfig{ projectCode = "TD" username = "username" password = "password" - doneTransitionCode = 41 } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt index b1115ff1df..9bc8d2e9f7 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/CertificationRequestStorage.kt @@ -59,7 +59,7 @@ interface CertificationRequestStorage { * @param rejectedBy authority (its identifier) rejecting this request. * @param rejectReason brief description of the rejection reason */ - fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) + fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String?) /** * Store certificate path with [requestId], this will store the encoded [CertPath] and transit request status to [RequestStatus.SIGNED]. diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt index 339fcb7978..f1511ff33f 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRequestStorage.kt @@ -93,7 +93,7 @@ class PersistentCertificateRequestStorage(private val database: CordaPersistence } } - override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String) { + override fun rejectRequest(requestId: String, rejectedBy: String, rejectReason: String?) { database.transaction(TransactionIsolationLevel.SERIALIZABLE) { val request = findRequest(requestId) request ?: throw IllegalArgumentException("Error when rejecting request with id: $requestId. Request does not exist.") diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt index 4ffaf397ce..5c2aabae0e 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt @@ -65,8 +65,7 @@ data class JiraConfig( val address: String, val projectCode: String, val username: String, - val password: String, - val doneTransitionCode: Int + val password: String ) /** diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt index d7ebf4abc0..9cc722a325 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/JiraCient.kt @@ -1,13 +1,15 @@ package com.r3.corda.networkmanage.doorman +import com.atlassian.jira.rest.client.api.IssueRestClient import com.atlassian.jira.rest.client.api.JiraRestClient +import com.atlassian.jira.rest.client.api.domain.Comment import com.atlassian.jira.rest.client.api.domain.Field import com.atlassian.jira.rest.client.api.domain.Issue import com.atlassian.jira.rest.client.api.domain.IssueType import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder import com.atlassian.jira.rest.client.api.domain.input.TransitionInput import net.corda.core.identity.CordaX500Name -import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.contextLogger import net.corda.nodeapi.internal.crypto.X509Utilities import org.bouncycastle.asn1.x500.style.BCStyle import org.bouncycastle.openssl.jcajce.JcaPEMWriter @@ -17,15 +19,20 @@ import java.io.StringWriter import java.security.cert.CertPath import javax.security.auth.x500.X500Principal -class JiraClient(private val restClient: JiraRestClient, private val projectCode: String, private val doneTransitionCode: Int) { +class JiraClient(private val restClient: JiraRestClient, private val projectCode: String) { companion object { - val logger = loggerFor() + val logger = contextLogger() } - // The JIRA project must have a Request ID field and the Task issue type. + // The JIRA project must have a Request ID and reject reason field, and the Task issue type. private val requestIdField: Field = restClient.metadataClient.fields.claim().find { it.name == "Request ID" } ?: throw IllegalArgumentException("Request ID field not found in JIRA '$projectCode'") + private val rejectReasonField: Field = restClient.metadataClient.fields.claim().find { it.name == "Reject Reason" } ?: throw IllegalArgumentException("Reject Reason field not found in JIRA '$projectCode'") private val taskIssueType: IssueType = restClient.metadataClient.issueTypes.claim().find { it.name == "Task" } ?: throw IllegalArgumentException("Task issue type field not found in JIRA '$projectCode'") + private var doneTransitionId: Int = -1 + private var canceledTransitionId: Int = -1 + private var startProgressTransitionId: Int = -1 + fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) { // Check there isn't already a ticket for this request. if (getIssueById(requestId) != null) { @@ -54,14 +61,26 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim() } - fun getApprovedRequests(): List> { + fun getApprovedRequests(): List { val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues - return issues.map { issue -> - issue.getField(requestIdField.id)?.value?.toString().let { - val requestId = it ?: throw IllegalArgumentException("RequestId cannot be null.") - val approvedBy = issue.assignee?.displayName ?: "Unknown" - Pair(requestId, approvedBy) - } + return issues.mapNotNull { issue -> + val requestId = issue.getField(requestIdField.id)?.value?.toString() ?: throw IllegalArgumentException("Error processing request '${issue.key}' : RequestId cannot be null.") + // Issue retrieved via search doesn't contain change logs. + val fullIssue = restClient.issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim() + val approvedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Approved" } } + ApprovedRequest(requestId, approvedBy?.author?.displayName ?: "Unknown") + } + } + + fun getRejectedRequests(): List { + val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Rejected").claim().issues + return issues.mapNotNull { issue -> + val requestId = issue.getField(requestIdField.id)?.value?.toString() ?: throw IllegalArgumentException("Error processing request '${issue.key}' : RequestId cannot be null.") + val rejectedReason = issue.getField(rejectReasonField.id)?.value?.toString() + // Issue retrieved via search doesn't contain comments. + val fullIssue = restClient.issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim() + val rejectedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Rejected" } } + RejectedRequest(requestId, rejectedBy?.author?.displayName ?: "Unknown", rejectedReason) } } @@ -69,16 +88,41 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode // Retrieving certificates for signed CSRs to attach to the jira tasks. signedRequests.forEach { (id, certPath) -> val certificate = certPath.certificates.first() - // Jira only support ~ (contains) search for custom textfield. val issue = getIssueById(id) if (issue != null) { - restClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim() - restClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer") - .fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim() + if (doneTransitionId == -1) { + doneTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Done" }.id + } + restClient.issueClient.transition(issue, TransitionInput(doneTransitionId)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim() + restClient.issueClient.addAttachment(issue.attachmentsUri, certificate.encoded.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer") + .fail { logger.error("Error processing request '${issue.key}' : Exception when uploading attachment to JIRA.", it) }.claim() } } } - private fun getIssueById(requestId: String): Issue? = - restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull() + fun updateRejectedRequests(rejectedRequests: List) { + rejectedRequests.mapNotNull { getIssueById(it) } + .forEach { issue -> + // Move status to in progress. + if (startProgressTransitionId == -1) { + startProgressTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Start Progress" }.id + } + restClient.issueClient.transition(issue, TransitionInput(startProgressTransitionId)).fail { logger.error("Error processing request '${issue.key}' : Exception when transiting JIRA status.", it) }.claim() + // Move status to cancelled. + if (canceledTransitionId == -1) { + canceledTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Stop Progress" }.id + } + restClient.issueClient.transition(issue, TransitionInput(canceledTransitionId)).fail { logger.error("Error processing request '${issue.key}' : Exception when transiting JIRA status.", it) }.claim() + restClient.issueClient.addComment(issue.commentsUri, Comment.valueOf("Request cancelled by doorman.")).claim() + } + } + + private fun getIssueById(requestId: String): Issue? { + // Jira only support ~ (contains) search for custom textfield. + return restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull() + } } + +data class ApprovedRequest(val requestId: String, val approvedBy: String) + +data class RejectedRequest(val requestId: String, val rejectedBy: String, val reason: String?) diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt index 030d92ce36..919ca722be 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/NetworkManagementServer.kt @@ -91,7 +91,7 @@ class NetworkManagementServer : Closeable { val jiraConfig = config.jiraConfig val requestProcessor = if (jiraConfig != null) { val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password) - val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode, jiraConfig.doneTransitionCode) + val jiraClient = JiraClient(jiraWebAPI, jiraConfig.projectCode) JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, csrCertPathAndKey)) } else { DefaultCsrHandler(requestService, csrCertPathAndKey) @@ -101,10 +101,8 @@ class NetworkManagementServer : Closeable { val approvalThread = Runnable { try { serverStatus.lastRequestCheckTime = Instant.now() - // Create tickets for requests which don't have one yet. - requestProcessor.createTickets() // Process Jira approved tickets. - requestProcessor.processApprovedRequests() + requestProcessor.processRequests() } catch (e: Exception) { // Log the error and carry on. logger.error("Error encountered when approving request.", e) diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt index 207a118c88..2048459c75 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/CsrHandler.kt @@ -18,25 +18,23 @@ import javax.security.auth.x500.X500Principal interface CsrHandler { fun saveRequest(rawRequest: PKCS10CertificationRequest): String - fun createTickets() - fun processApprovedRequests() + fun processRequests() fun getResponse(requestId: String): CertificateResponse } class DefaultCsrHandler(private val storage: CertificationRequestStorage, private val csrCertPathAndKey: CertPathAndKey?) : CsrHandler { - override fun processApprovedRequests() { + override fun processRequests() { if (csrCertPathAndKey == null) return storage.getRequests(RequestStatus.APPROVED).forEach { val nodeCertPath = createSignedNodeCertificate(it.request, csrCertPathAndKey) - // Since Doorman is deployed in the auto-signing mode, we use DOORMAN_SIGNATURE as the signer. + // Since Doorman is deployed in the auto-signing mode (i.e. signer != null), + // we use DOORMAN_SIGNATURE as the signer. storage.putCertificatePath(it.requestId, nodeCertPath, listOf(DOORMAN_SIGNATURE)) } } - override fun createTickets() {} - override fun saveRequest(rawRequest: PKCS10CertificationRequest): String = storage.saveRequest(rawRequest) override fun getResponse(requestId: String): CertificateResponse { diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt index e7f9d51046..b7bea5490e 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandler.kt @@ -4,7 +4,9 @@ import com.r3.corda.networkmanage.common.persistence.CertificateResponse import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage import com.r3.corda.networkmanage.common.persistence.RequestStatus +import com.r3.corda.networkmanage.doorman.ApprovedRequest import com.r3.corda.networkmanage.doorman.JiraClient +import com.r3.corda.networkmanage.doorman.RejectedRequest import net.corda.core.utilities.contextLogger import net.corda.core.utilities.loggerFor import org.bouncycastle.pkcs.PKCS10CertificationRequest @@ -29,21 +31,34 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce } } - override fun processApprovedRequests() { + override fun processRequests() { + createTickets() + val (approvedRequests, rejectedRequests) = updateRequestStatus() + delegate.processRequests() + updateJiraTickets(approvedRequests, rejectedRequests) + } + + private fun updateRequestStatus(): Pair, List> { + // Update local request statuses. val approvedRequest = jiraClient.getApprovedRequests() approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) } - delegate.processApprovedRequests() + val rejectedRequest = jiraClient.getRejectedRequests() + rejectedRequest.forEach { (id, rejectedBy, reason) -> storage.rejectRequest(id, rejectedBy, reason) } + return Pair(approvedRequest, rejectedRequest) + } - val signedRequests = approvedRequest.mapNotNull { (id, _) -> - val request = storage.getRequest(id) - - if (request != null && request.status == RequestStatus.SIGNED) { - request.certData?.certPath?.let { certs -> id to certs } - } else { - null - } - }.toMap() + private fun updateJiraTickets(approvedRequest: List, rejectedRequest: List) { + // Reconfirm request status and update jira status + val signedRequests = approvedRequest.mapNotNull { storage.getRequest(it.requestId) } + .filter { it.status == RequestStatus.SIGNED && it.certData != null } + .associateBy { it.requestId } + .mapValues { it.value.certData!!.certPath } jiraClient.updateSignedRequests(signedRequests) + + val rejectedRequestIDs = rejectedRequest.mapNotNull { storage.getRequest(it.requestId) } + .filter { it.status == RequestStatus.REJECTED } + .map { it.requestId } + jiraClient.updateRejectedRequests(rejectedRequestIDs) } /** @@ -52,13 +67,13 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce * Usually requests are expected to move to the [RequestStatus.TICKET_CREATED] state immediately, * they might be left in the [RequestStatus.NEW] state if Jira is down. */ - override fun createTickets() { - try { - for (signingRequest in storage.getRequests(RequestStatus.NEW)) { - createTicket(signingRequest) + private fun createTickets() { + storage.getRequests(RequestStatus.NEW).forEach { + try { + createTicket(it) + } catch (e: Exception) { + log.warn("There were errors while creating Jira tickets for request '${it.requestId}'", e) } - } catch (e: Exception) { - log.warn("There were errors while creating Jira tickets", e) } } diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt index 93706599c5..cf881f2712 100644 --- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt +++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/NetworkMapWebService.kt @@ -86,7 +86,6 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage, @GET @Path("my-ip") fun myIp(@Context request: HttpServletRequest): Response { - // TODO: Verify this returns IP correctly. return ok(request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}").build() } diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt index d2b92b752a..d44ff5304a 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/DoormanParametersTest.kt @@ -52,6 +52,5 @@ class DoormanParametersTest { assertEquals("TD", parameter.jiraConfig?.projectCode) assertEquals("username", parameter.jiraConfig?.username) assertEquals("password", parameter.jiraConfig?.password) - assertEquals(41, parameter.jiraConfig?.doneTransitionCode) } } diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/JiraClientTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/JiraClientTest.kt new file mode 100644 index 0000000000..d3558d0c6e --- /dev/null +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/JiraClientTest.kt @@ -0,0 +1,55 @@ +package com.r3.corda.networkmanage.doorman + +import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory +import com.r3.corda.networkmanage.common.utils.buildCertPath +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.CordaX500Name +import net.corda.nodeapi.internal.crypto.X509Utilities +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import java.net.URI + +@Ignore +// This is manual test for testing Jira API. +class JiraClientTest { + private lateinit var jiraClient: JiraClient + @Before + fun init() { + val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI("http://jira.url.com"), "username", "password") + jiraClient = JiraClient(jiraWebAPI, "DOOR") + } + + @Test + fun createRequestTicket() { + val request = X509Utilities.createCertificateSigningRequest(CordaX500Name("JiraAPITest", "R3 Ltd 3", "London", "GB").x500Principal, "test@test.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + jiraClient.createRequestTicket(SecureHash.randomSHA256().toString(), request) + } + + @Test + fun getApprovedRequests() { + jiraClient.getApprovedRequests().forEach { println(it) } + } + + @Test + fun getRejectedRequests() { + val requests = jiraClient.getRejectedRequests() + requests.forEach { println(it) } + } + + @Test + fun updateSignedRequests() { + val requests = jiraClient.getApprovedRequests() + val selfSignedCA = X509Utilities.createSelfSignedCACertificate(CordaX500Name("test", "london", "GB").x500Principal, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + jiraClient.updateSignedRequests(requests.map { it.requestId to buildCertPath(selfSignedCA) }.toMap()) + } + + @Test + fun updateRejectedRequests() { + val requests = jiraClient.getRejectedRequests() + jiraClient.updateRejectedRequests(requests.map { it.requestId }) + + assert(jiraClient.getRejectedRequests().isEmpty()) + } +} diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt index e518801bec..67af01bd64 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/DefaultCsrHandlerTest.kt @@ -62,7 +62,7 @@ class DefaultCsrHandlerTest : TestBase() { val csrCertPathAndKey = CertPathAndKey(listOf(csrCa.certificate, rootCa.certificate), csrCa.keyPair.private) val requestProcessor = DefaultCsrHandler(requestStorage, csrCertPathAndKey) - requestProcessor.processApprovedRequests() + requestProcessor.processRequests() val certPathCapture = argumentCaptor() diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt index 19498bb744..02b6566774 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/signer/JiraCsrHandlerTest.kt @@ -1,12 +1,12 @@ package com.r3.corda.networkmanage.doorman.signer import com.nhaarman.mockito_kotlin.* -import com.r3.corda.networkmanage.common.persistence.CertificateResponse -import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest -import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage -import com.r3.corda.networkmanage.common.persistence.RequestStatus +import com.r3.corda.networkmanage.common.persistence.* +import com.r3.corda.networkmanage.doorman.ApprovedRequest import com.r3.corda.networkmanage.doorman.JiraClient +import com.r3.corda.networkmanage.doorman.RejectedRequest import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash import net.corda.core.identity.CordaX500Name import net.corda.nodeapi.internal.crypto.X509Utilities import org.junit.Before @@ -16,9 +16,9 @@ import org.mockito.Mock import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule import java.security.cert.CertPath +import kotlin.test.assertEquals class JiraCsrHandlerTest { - @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule() @@ -80,9 +80,67 @@ class JiraCsrHandlerTest { whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr)) // Test - jiraCsrHandler.createTickets() + jiraCsrHandler.processRequests() verify(jiraClient).createRequestTicket(requestId, csr.request) verify(certificationRequestStorage).markRequestTicketCreated(requestId) } -} \ No newline at end of file + + @Test + fun `sync tickets status`() { + val id1 = SecureHash.randomSHA256().toString() + val id2 = SecureHash.randomSHA256().toString() + val csr1 = CertificateSigningRequest(id1, "name1", RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null) + val csr2 = CertificateSigningRequest(id2, "name2", RequestStatus.NEW, pkcS10CertificationRequest, null, emptyList(), null) + + val requests = mutableMapOf(id1 to csr1, id2 to csr2) + + // Mocking storage behaviour. + whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(requests.values.filter { it.status == RequestStatus.NEW }) + whenever(certificationRequestStorage.getRequest(any())).thenAnswer { requests[it.getArgument(0)] } + whenever(certificationRequestStorage.approveRequest(any(), any())).then { + val id = it.getArgument(0) + if (requests[id]?.status == RequestStatus.NEW) { + requests[id] = requests[id]!!.copy(status = RequestStatus.APPROVED, modifiedBy = listOf(it.getArgument(1))) + } + null + } + whenever(certificationRequestStorage.rejectRequest(any(), any(), any())).then { + val id = it.getArgument(0) + requests[id] = requests[id]!!.copy(status = RequestStatus.REJECTED, modifiedBy = listOf(it.getArgument(1)), remark = it.getArgument(2)) + null + } + + // Status change from jira. + whenever(jiraClient.getApprovedRequests()).thenReturn(listOf(ApprovedRequest(id1, "Me"))) + whenever(jiraClient.getRejectedRequests()).thenReturn(listOf(RejectedRequest(id2, "Me", "Test reject"))) + + // Test. + jiraCsrHandler.processRequests() + + verify(jiraClient).createRequestTicket(id1, csr1.request) + verify(jiraClient).createRequestTicket(id2, csr2.request) + + verify(certificationRequestStorage).markRequestTicketCreated(id1) + verify(certificationRequestStorage).markRequestTicketCreated(id2) + + // Verify request has the correct status in DB. + assertEquals(RequestStatus.APPROVED, requests[id1]!!.status) + assertEquals(RequestStatus.REJECTED, requests[id2]!!.status) + + // Verify jira client get the correct call. + verify(jiraClient).updateRejectedRequests(listOf(id2)) + verify(jiraClient).updateSignedRequests(emptyMap()) + + // Sign request 1 + val certPath = mock() + val certData = CertificateData("", CertificateStatus.VALID, certPath) + requests[id1] = requests[id1]!!.copy(status = RequestStatus.SIGNED, certData = certData) + + // Process request again. + jiraCsrHandler.processRequests() + + // Update signed request should be called. + verify(jiraClient).updateSignedRequests(mapOf(id1 to certPath)) + } +} diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt index d9821f38fc..ead1e2f91a 100644 --- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt +++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/doorman/webservice/RegistrationWebServiceTest.kt @@ -100,7 +100,7 @@ class RegistrationWebServiceTest : TestBase() { CertificateResponse.Ready(it) } ?: CertificateResponse.NotReady } - on { processApprovedRequests() }.then { + on { processRequests() }.then { val request = X509Utilities.createCertificateSigningRequest(subject, "my@mail.com", keyPair) certificateStore[id] = JcaPKCS10CertificationRequest(request).run { val tlsCert = X509Utilities.createCertificate( @@ -118,7 +118,7 @@ class RegistrationWebServiceTest : TestBase() { startSigningServer(requestProcessor) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) - requestProcessor.processApprovedRequests() + requestProcessor.processRequests() val certificates = (pollForResponse(id) as PollResponse.Ready).certChain verify(requestProcessor, times(2)).getResponse(any()) @@ -141,7 +141,7 @@ class RegistrationWebServiceTest : TestBase() { CertificateResponse.Ready(it) } ?: CertificateResponse.NotReady } - on { processApprovedRequests() }.then { + on { processRequests() }.then { val request = X509Utilities.createCertificateSigningRequest( CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB").x500Principal, "my@mail.com", @@ -165,7 +165,7 @@ class RegistrationWebServiceTest : TestBase() { startSigningServer(storage) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) - storage.processApprovedRequests() + storage.processRequests() val certificates = (pollForResponse(id) as PollResponse.Ready).certChain verify(storage, times(2)).getResponse(any())