mirror of
https://github.com/corda/corda.git
synced 2025-01-15 01:10:33 +00:00
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
This commit is contained in:
parent
3094e44115
commit
43604ed212
@ -65,13 +65,15 @@ The doorman service can use JIRA to manage the certificate signing request appro
|
|||||||
projectCode = "TD"
|
projectCode = "TD"
|
||||||
username = "username"
|
username = "username"
|
||||||
password = "password"
|
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
|
### 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)
|
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"
|
projectCode = "TD"
|
||||||
username = "username"
|
username = "username"
|
||||||
password = "password"
|
password = "password"
|
||||||
doneTransitionCode = 41
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ dependencies {
|
|||||||
testCompile "com.nhaarman:mockito-kotlin:0.6.1"
|
testCompile "com.nhaarman:mockito-kotlin:0.6.1"
|
||||||
testCompile "com.spotify:docker-client:8.9.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.
|
// The jira client includes jersey-core 1.5 which breaks everything.
|
||||||
exclude module: 'jersey-core'
|
exclude module: 'jersey-core'
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,6 @@ doormanConfig{
|
|||||||
projectCode = "TD"
|
projectCode = "TD"
|
||||||
username = "username"
|
username = "username"
|
||||||
password = "password"
|
password = "password"
|
||||||
doneTransitionCode = 41
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ interface CertificationRequestStorage {
|
|||||||
* @param rejectedBy authority (its identifier) rejecting this request.
|
* @param rejectedBy authority (its identifier) rejecting this request.
|
||||||
* @param rejectReason brief description of the rejection reason
|
* @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].
|
* Store certificate path with [requestId], this will store the encoded [CertPath] and transit request status to [RequestStatus.SIGNED].
|
||||||
|
@ -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) {
|
database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
|
||||||
val request = findRequest(requestId)
|
val request = findRequest(requestId)
|
||||||
request ?: throw IllegalArgumentException("Error when rejecting request with id: $requestId. Request does not exist.")
|
request ?: throw IllegalArgumentException("Error when rejecting request with id: $requestId. Request does not exist.")
|
||||||
|
@ -65,8 +65,7 @@ data class JiraConfig(
|
|||||||
val address: String,
|
val address: String,
|
||||||
val projectCode: String,
|
val projectCode: String,
|
||||||
val username: String,
|
val username: String,
|
||||||
val password: String,
|
val password: String
|
||||||
val doneTransitionCode: Int
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
package com.r3.corda.networkmanage.doorman
|
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.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.Field
|
||||||
import com.atlassian.jira.rest.client.api.domain.Issue
|
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.IssueType
|
||||||
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
|
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
|
||||||
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
|
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
|
||||||
import net.corda.core.identity.CordaX500Name
|
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 net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
import org.bouncycastle.asn1.x500.style.BCStyle
|
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
||||||
@ -17,15 +19,20 @@ import java.io.StringWriter
|
|||||||
import java.security.cert.CertPath
|
import java.security.cert.CertPath
|
||||||
import javax.security.auth.x500.X500Principal
|
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 {
|
companion object {
|
||||||
val logger = loggerFor<JiraClient>()
|
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 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 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) {
|
fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) {
|
||||||
// Check there isn't already a ticket for this request.
|
// Check there isn't already a ticket for this request.
|
||||||
if (getIssueById(requestId) != null) {
|
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()
|
restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getApprovedRequests(): List<Pair<String, String>> {
|
fun getApprovedRequests(): List<ApprovedRequest> {
|
||||||
val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues
|
val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues
|
||||||
return issues.map { issue ->
|
return issues.mapNotNull { issue ->
|
||||||
issue.getField(requestIdField.id)?.value?.toString().let {
|
val requestId = issue.getField(requestIdField.id)?.value?.toString() ?: throw IllegalArgumentException("Error processing request '${issue.key}' : RequestId cannot be null.")
|
||||||
val requestId = it ?: throw IllegalArgumentException("RequestId cannot be null.")
|
// Issue retrieved via search doesn't contain change logs.
|
||||||
val approvedBy = issue.assignee?.displayName ?: "Unknown"
|
val fullIssue = restClient.issueClient.getIssue(issue.key, listOf(IssueRestClient.Expandos.CHANGELOG)).claim()
|
||||||
Pair(requestId, approvedBy)
|
val approvedBy = fullIssue.changelog?.last { it.items.any { it.field == "status" && it.toString == "Approved" } }
|
||||||
}
|
ApprovedRequest(requestId, approvedBy?.author?.displayName ?: "Unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRejectedRequests(): List<RejectedRequest> {
|
||||||
|
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.
|
// Retrieving certificates for signed CSRs to attach to the jira tasks.
|
||||||
signedRequests.forEach { (id, certPath) ->
|
signedRequests.forEach { (id, certPath) ->
|
||||||
val certificate = certPath.certificates.first()
|
val certificate = certPath.certificates.first()
|
||||||
// Jira only support ~ (contains) search for custom textfield.
|
|
||||||
val issue = getIssueById(id)
|
val issue = getIssueById(id)
|
||||||
if (issue != null) {
|
if (issue != null) {
|
||||||
restClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
|
if (doneTransitionId == -1) {
|
||||||
restClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
|
doneTransitionId = restClient.issueClient.getTransitions(issue.transitionsUri).claim().single { it.name == "Done" }.id
|
||||||
.fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim()
|
}
|
||||||
|
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? =
|
fun updateRejectedRequests(rejectedRequests: List<String>) {
|
||||||
restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
|
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?)
|
||||||
|
@ -91,7 +91,7 @@ class NetworkManagementServer : Closeable {
|
|||||||
val jiraConfig = config.jiraConfig
|
val jiraConfig = config.jiraConfig
|
||||||
val requestProcessor = if (jiraConfig != null) {
|
val requestProcessor = if (jiraConfig != null) {
|
||||||
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
|
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))
|
JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, csrCertPathAndKey))
|
||||||
} else {
|
} else {
|
||||||
DefaultCsrHandler(requestService, csrCertPathAndKey)
|
DefaultCsrHandler(requestService, csrCertPathAndKey)
|
||||||
@ -101,10 +101,8 @@ class NetworkManagementServer : Closeable {
|
|||||||
val approvalThread = Runnable {
|
val approvalThread = Runnable {
|
||||||
try {
|
try {
|
||||||
serverStatus.lastRequestCheckTime = Instant.now()
|
serverStatus.lastRequestCheckTime = Instant.now()
|
||||||
// Create tickets for requests which don't have one yet.
|
|
||||||
requestProcessor.createTickets()
|
|
||||||
// Process Jira approved tickets.
|
// Process Jira approved tickets.
|
||||||
requestProcessor.processApprovedRequests()
|
requestProcessor.processRequests()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Log the error and carry on.
|
// Log the error and carry on.
|
||||||
logger.error("Error encountered when approving request.", e)
|
logger.error("Error encountered when approving request.", e)
|
||||||
|
@ -18,25 +18,23 @@ import javax.security.auth.x500.X500Principal
|
|||||||
|
|
||||||
interface CsrHandler {
|
interface CsrHandler {
|
||||||
fun saveRequest(rawRequest: PKCS10CertificationRequest): String
|
fun saveRequest(rawRequest: PKCS10CertificationRequest): String
|
||||||
fun createTickets()
|
fun processRequests()
|
||||||
fun processApprovedRequests()
|
|
||||||
fun getResponse(requestId: String): CertificateResponse
|
fun getResponse(requestId: String): CertificateResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultCsrHandler(private val storage: CertificationRequestStorage,
|
class DefaultCsrHandler(private val storage: CertificationRequestStorage,
|
||||||
private val csrCertPathAndKey: CertPathAndKey?) : CsrHandler {
|
private val csrCertPathAndKey: CertPathAndKey?) : CsrHandler {
|
||||||
|
|
||||||
override fun processApprovedRequests() {
|
override fun processRequests() {
|
||||||
if (csrCertPathAndKey == null) return
|
if (csrCertPathAndKey == null) return
|
||||||
storage.getRequests(RequestStatus.APPROVED).forEach {
|
storage.getRequests(RequestStatus.APPROVED).forEach {
|
||||||
val nodeCertPath = createSignedNodeCertificate(it.request, csrCertPathAndKey)
|
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))
|
storage.putCertificatePath(it.requestId, nodeCertPath, listOf(DOORMAN_SIGNATURE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createTickets() {}
|
|
||||||
|
|
||||||
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String = storage.saveRequest(rawRequest)
|
override fun saveRequest(rawRequest: PKCS10CertificationRequest): String = storage.saveRequest(rawRequest)
|
||||||
|
|
||||||
override fun getResponse(requestId: String): CertificateResponse {
|
override fun getResponse(requestId: String): CertificateResponse {
|
||||||
|
@ -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.CertificateSigningRequest
|
||||||
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
|
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
|
||||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
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.JiraClient
|
||||||
|
import com.r3.corda.networkmanage.doorman.RejectedRequest
|
||||||
import net.corda.core.utilities.contextLogger
|
import net.corda.core.utilities.contextLogger
|
||||||
import net.corda.core.utilities.loggerFor
|
import net.corda.core.utilities.loggerFor
|
||||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
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<ApprovedRequest>, List<RejectedRequest>> {
|
||||||
|
// Update local request statuses.
|
||||||
val approvedRequest = jiraClient.getApprovedRequests()
|
val approvedRequest = jiraClient.getApprovedRequests()
|
||||||
approvedRequest.forEach { (id, approvedBy) -> storage.approveRequest(id, approvedBy) }
|
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, _) ->
|
private fun updateJiraTickets(approvedRequest: List<ApprovedRequest>, rejectedRequest: List<RejectedRequest>) {
|
||||||
val request = storage.getRequest(id)
|
// Reconfirm request status and update jira status
|
||||||
|
val signedRequests = approvedRequest.mapNotNull { storage.getRequest(it.requestId) }
|
||||||
if (request != null && request.status == RequestStatus.SIGNED) {
|
.filter { it.status == RequestStatus.SIGNED && it.certData != null }
|
||||||
request.certData?.certPath?.let { certs -> id to certs }
|
.associateBy { it.requestId }
|
||||||
} else {
|
.mapValues { it.value.certData!!.certPath }
|
||||||
null
|
|
||||||
}
|
|
||||||
}.toMap()
|
|
||||||
jiraClient.updateSignedRequests(signedRequests)
|
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,
|
* 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.
|
* they might be left in the [RequestStatus.NEW] state if Jira is down.
|
||||||
*/
|
*/
|
||||||
override fun createTickets() {
|
private fun createTickets() {
|
||||||
try {
|
storage.getRequests(RequestStatus.NEW).forEach {
|
||||||
for (signingRequest in storage.getRequests(RequestStatus.NEW)) {
|
try {
|
||||||
createTicket(signingRequest)
|
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +86,6 @@ class NetworkMapWebService(private val nodeInfoStorage: NodeInfoStorage,
|
|||||||
@GET
|
@GET
|
||||||
@Path("my-ip")
|
@Path("my-ip")
|
||||||
fun myIp(@Context request: HttpServletRequest): Response {
|
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()
|
return ok(request.getHeader("X-Forwarded-For")?.split(",")?.first() ?: "${request.remoteHost}:${request.remotePort}").build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +52,5 @@ class DoormanParametersTest {
|
|||||||
assertEquals("TD", parameter.jiraConfig?.projectCode)
|
assertEquals("TD", parameter.jiraConfig?.projectCode)
|
||||||
assertEquals("username", parameter.jiraConfig?.username)
|
assertEquals("username", parameter.jiraConfig?.username)
|
||||||
assertEquals("password", parameter.jiraConfig?.password)
|
assertEquals("password", parameter.jiraConfig?.password)
|
||||||
assertEquals(41, parameter.jiraConfig?.doneTransitionCode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
@ -62,7 +62,7 @@ class DefaultCsrHandlerTest : TestBase() {
|
|||||||
val csrCertPathAndKey = CertPathAndKey(listOf(csrCa.certificate, rootCa.certificate), csrCa.keyPair.private)
|
val csrCertPathAndKey = CertPathAndKey(listOf(csrCa.certificate, rootCa.certificate), csrCa.keyPair.private)
|
||||||
val requestProcessor = DefaultCsrHandler(requestStorage, csrCertPathAndKey)
|
val requestProcessor = DefaultCsrHandler(requestStorage, csrCertPathAndKey)
|
||||||
|
|
||||||
requestProcessor.processApprovedRequests()
|
requestProcessor.processRequests()
|
||||||
|
|
||||||
val certPathCapture = argumentCaptor<CertPath>()
|
val certPathCapture = argumentCaptor<CertPath>()
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
package com.r3.corda.networkmanage.doorman.signer
|
package com.r3.corda.networkmanage.doorman.signer
|
||||||
|
|
||||||
import com.nhaarman.mockito_kotlin.*
|
import com.nhaarman.mockito_kotlin.*
|
||||||
import com.r3.corda.networkmanage.common.persistence.CertificateResponse
|
import com.r3.corda.networkmanage.common.persistence.*
|
||||||
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
|
import com.r3.corda.networkmanage.doorman.ApprovedRequest
|
||||||
import com.r3.corda.networkmanage.common.persistence.CertificationRequestStorage
|
|
||||||
import com.r3.corda.networkmanage.common.persistence.RequestStatus
|
|
||||||
import com.r3.corda.networkmanage.doorman.JiraClient
|
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.Crypto
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.identity.CordaX500Name
|
import net.corda.core.identity.CordaX500Name
|
||||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
@ -16,9 +16,9 @@ import org.mockito.Mock
|
|||||||
import org.mockito.junit.MockitoJUnit
|
import org.mockito.junit.MockitoJUnit
|
||||||
import org.mockito.junit.MockitoRule
|
import org.mockito.junit.MockitoRule
|
||||||
import java.security.cert.CertPath
|
import java.security.cert.CertPath
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
class JiraCsrHandlerTest {
|
class JiraCsrHandlerTest {
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
val mockitoRule: MockitoRule = MockitoJUnit.rule()
|
val mockitoRule: MockitoRule = MockitoJUnit.rule()
|
||||||
@ -80,9 +80,67 @@ class JiraCsrHandlerTest {
|
|||||||
whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr))
|
whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr))
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
jiraCsrHandler.createTickets()
|
jiraCsrHandler.processRequests()
|
||||||
|
|
||||||
verify(jiraClient).createRequestTicket(requestId, csr.request)
|
verify(jiraClient).createRequestTicket(requestId, csr.request)
|
||||||
verify(certificationRequestStorage).markRequestTicketCreated(requestId)
|
verify(certificationRequestStorage).markRequestTicketCreated(requestId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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<String>(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<String>(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<CertPath>()
|
||||||
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
@ -100,7 +100,7 @@ class RegistrationWebServiceTest : TestBase() {
|
|||||||
CertificateResponse.Ready(it)
|
CertificateResponse.Ready(it)
|
||||||
} ?: CertificateResponse.NotReady
|
} ?: CertificateResponse.NotReady
|
||||||
}
|
}
|
||||||
on { processApprovedRequests() }.then {
|
on { processRequests() }.then {
|
||||||
val request = X509Utilities.createCertificateSigningRequest(subject, "my@mail.com", keyPair)
|
val request = X509Utilities.createCertificateSigningRequest(subject, "my@mail.com", keyPair)
|
||||||
certificateStore[id] = JcaPKCS10CertificationRequest(request).run {
|
certificateStore[id] = JcaPKCS10CertificationRequest(request).run {
|
||||||
val tlsCert = X509Utilities.createCertificate(
|
val tlsCert = X509Utilities.createCertificate(
|
||||||
@ -118,7 +118,7 @@ class RegistrationWebServiceTest : TestBase() {
|
|||||||
startSigningServer(requestProcessor)
|
startSigningServer(requestProcessor)
|
||||||
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
|
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
|
||||||
|
|
||||||
requestProcessor.processApprovedRequests()
|
requestProcessor.processRequests()
|
||||||
|
|
||||||
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
|
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
|
||||||
verify(requestProcessor, times(2)).getResponse(any())
|
verify(requestProcessor, times(2)).getResponse(any())
|
||||||
@ -141,7 +141,7 @@ class RegistrationWebServiceTest : TestBase() {
|
|||||||
CertificateResponse.Ready(it)
|
CertificateResponse.Ready(it)
|
||||||
} ?: CertificateResponse.NotReady
|
} ?: CertificateResponse.NotReady
|
||||||
}
|
}
|
||||||
on { processApprovedRequests() }.then {
|
on { processRequests() }.then {
|
||||||
val request = X509Utilities.createCertificateSigningRequest(
|
val request = X509Utilities.createCertificateSigningRequest(
|
||||||
CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB").x500Principal,
|
CordaX500Name(locality = "London", organisation = "Legal Name", country = "GB").x500Principal,
|
||||||
"my@mail.com",
|
"my@mail.com",
|
||||||
@ -165,7 +165,7 @@ class RegistrationWebServiceTest : TestBase() {
|
|||||||
|
|
||||||
startSigningServer(storage)
|
startSigningServer(storage)
|
||||||
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
|
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
|
||||||
storage.processApprovedRequests()
|
storage.processRequests()
|
||||||
|
|
||||||
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
|
val certificates = (pollForResponse(id) as PollResponse.Ready).certChain
|
||||||
verify(storage, times(2)).getResponse(any())
|
verify(storage, times(2)).getResponse(any())
|
||||||
|
Loading…
Reference in New Issue
Block a user