Adding jira integration to CRL (#579)

* Adding jira integration

* Addressing review comments

* Adding review comments

* Fixes after rebase
This commit is contained in:
Michal Kit 2018-03-23 12:54:38 +00:00 committed by GitHub
parent c9caaf8be4
commit 101eee1ff5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 511 additions and 122 deletions

View File

@ -146,6 +146,7 @@ class NetworkParametersUpdateTest : IntegrationTest() {
serverAddress, serverAddress,
CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private), CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private),
DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis), DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis),
null,
if (startNetworkMap) { if (startNetworkMap) {
NetworkMapStartParams( NetworkMapStartParams(
LocalSigner(networkMapCa), LocalSigner(networkMapCa),

View File

@ -159,6 +159,13 @@ class NodeRegistrationTest : IntegrationTest() {
serverAddress, serverAddress,
CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private), CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private),
DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis), DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis),
CertificateRevocationConfig(
approveAll = true,
jira = null,
approveInterval = timeoutMillis,
crlCacheTimeout = timeoutMillis,
crlEndpoint = URL("http://test.com/crl"),
crlUpdateInterval = timeoutMillis),
if (startNetworkMap) { if (startNetworkMap) {
NetworkMapStartParams( NetworkMapStartParams(
LocalSigner(networkMapCa), LocalSigner(networkMapCa),

View File

@ -14,6 +14,7 @@ import com.nhaarman.mockito_kotlin.*
import com.r3.corda.networkmanage.common.HOST import com.r3.corda.networkmanage.common.HOST
import com.r3.corda.networkmanage.common.HsmBaseTest import com.r3.corda.networkmanage.common.HsmBaseTest
import com.r3.corda.networkmanage.common.persistence.configureDatabase import com.r3.corda.networkmanage.common.persistence.configureDatabase
import com.r3.corda.networkmanage.doorman.CertificateRevocationConfig
import com.r3.corda.networkmanage.doorman.DoormanConfig import com.r3.corda.networkmanage.doorman.DoormanConfig
import com.r3.corda.networkmanage.doorman.NetworkManagementServer import com.r3.corda.networkmanage.doorman.NetworkManagementServer
import com.r3.corda.networkmanage.hsm.persistence.ApprovedCertificateRequestData import com.r3.corda.networkmanage.hsm.persistence.ApprovedCertificateRequestData
@ -26,6 +27,8 @@ import net.corda.core.internal.createDirectories
import net.corda.core.internal.div import net.corda.core.internal.div
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.uncheckedCast
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.hours
import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.node.NodeRegistrationOption import net.corda.node.NodeRegistrationOption
import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.NodeConfiguration
@ -101,6 +104,14 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
hostAndPort = NetworkHostAndPort(HOST, 0), hostAndPort = NetworkHostAndPort(HOST, 0),
csrCertPathAndKey = null, csrCertPathAndKey = null,
doormanConfig = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jira = null), doormanConfig = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jira = null),
revocationConfig = CertificateRevocationConfig(
approveAll = true,
jira = null,
crlUpdateInterval = 2.hours.toMillis(),
crlCacheTimeout = 30.minutes.toMillis(),
crlEndpoint = URL("http://test.com/crl"),
approveInterval = 10.minutes.toMillis()
),
startNetworkMap = null) startNetworkMap = null)
val doormanHostAndPort = server.hostAndPort val doormanHostAndPort = server.hostAndPort
// Start Corda network registration. // Start Corda network registration.

View File

@ -0,0 +1,17 @@
package com.r3.corda.networkmanage.common.persistence
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestStorage.Companion.DOORMAN_SIGNATURE
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
/**
* This storage automatically approves all created requests.
*/
class ApproveAllCertificateRevocationRequestStorage(private val delegate: CertificateRevocationRequestStorage) : CertificateRevocationRequestStorage by delegate {
override fun saveRevocationRequest(request: CertificateRevocationRequest): String {
val requestId = delegate.saveRevocationRequest(request)
delegate.markRequestTicketCreated(requestId)
approveRevocationRequest(requestId, DOORMAN_SIGNATURE)
return requestId
}
}

View File

@ -7,6 +7,10 @@ import java.time.Instant
* Interface for managing certificate revocation list persistence * Interface for managing certificate revocation list persistence
*/ */
interface CertificateRevocationListStorage { interface CertificateRevocationListStorage {
companion object {
val DOORMAN_SIGNATURE = "Doorman-Crl-Signer"
}
/** /**
* Retrieves the latest certificate revocation list. * Retrieves the latest certificate revocation list.
* *

View File

@ -5,6 +5,7 @@ import net.corda.core.serialization.CordaSerializable
import net.corda.nodeapi.internal.network.CertificateRevocationRequest import net.corda.nodeapi.internal.network.CertificateRevocationRequest
import java.math.BigInteger import java.math.BigInteger
import java.security.cert.CRLReason import java.security.cert.CRLReason
import java.security.cert.X509Certificate
import java.time.Instant import java.time.Instant
/** /**
@ -12,6 +13,8 @@ import java.time.Instant
*/ */
@CordaSerializable @CordaSerializable
data class CertificateRevocationRequestData(val requestId: String, // This is a uniquely generated string data class CertificateRevocationRequestData(val requestId: String, // This is a uniquely generated string
val certificateSigningRequestId: String,
val certificate: X509Certificate,
val certificateSerialNumber: BigInteger, val certificateSerialNumber: BigInteger,
val modifiedAt: Instant, val modifiedAt: Instant,
val legalName: CordaX500Name, val legalName: CordaX500Name,
@ -23,6 +26,9 @@ data class CertificateRevocationRequestData(val requestId: String, // This is a
* Interface for managing certificate revocation requests persistence * Interface for managing certificate revocation requests persistence
*/ */
interface CertificateRevocationRequestStorage { interface CertificateRevocationRequestStorage {
companion object {
val DOORMAN_SIGNATURE = "Doorman-Crr-Signer"
}
/** /**
* Creates a new revocation request for the given [certificateSerialNumber]. * Creates a new revocation request for the given [certificateSerialNumber].
@ -70,5 +76,10 @@ interface CertificateRevocationRequestStorage {
* @param rejectedBy who is rejecting it * @param rejectedBy who is rejecting it
* @param reason description of the reason of this rejection. * @param reason description of the reason of this rejection.
*/ */
fun rejectRevocationRequest(requestId: String, rejectedBy: String, reason: String) fun rejectRevocationRequest(requestId: String, rejectedBy: String, reason: String?)
/**
* Persist the fact that a ticket has been created for the given [requestId].
*/
fun markRequestTicketCreated(requestId: String)
} }

View File

@ -32,7 +32,7 @@ data class CertificateSigningRequest(val requestId: String,
*/ */
interface CertificateSigningRequestStorage { interface CertificateSigningRequestStorage {
companion object { companion object {
val DOORMAN_SIGNATURE = "Doorman" val DOORMAN_SIGNATURE = "Doorman-Csr-Signer"
} }
/** /**

View File

@ -10,6 +10,7 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseTransaction import net.corda.nodeapi.internal.persistence.DatabaseTransaction
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
import java.math.BigInteger import java.math.BigInteger
import java.security.cert.X509Certificate
import java.time.Instant import java.time.Instant
class PersistentCertificateRevocationRequestStorage(private val database: CordaPersistence) : CertificateRevocationRequestStorage { class PersistentCertificateRevocationRequestStorage(private val database: CordaPersistence) : CertificateRevocationRequestStorage {
@ -129,7 +130,7 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
} }
} }
override fun rejectRevocationRequest(requestId: String, rejectedBy: String, reason: String) { override fun rejectRevocationRequest(requestId: String, rejectedBy: String, reason: String?) {
database.transaction { database.transaction {
val revocation = getRevocationRequestEntity(requestId) val revocation = getRevocationRequestEntity(requestId)
if (revocation == null) { if (revocation == null) {
@ -145,10 +146,28 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
} }
} }
private fun getRevocationRequestEntity(requestId: String): CertificateRevocationRequestEntity? { override fun markRequestTicketCreated(requestId: String) {
// Even though, we have an assumption that there is always a single instance of the doorman service running,
// the SERIALIZABLE isolation level is used here just to ensure data consistency between the updates.
return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
val request = getRevocationRequestEntity(requestId, RequestStatus.NEW)
request ?: throw IllegalArgumentException("Error when creating request ticket with id: $requestId. Request does not exist or its status is not NEW.")
val update = request.copy(
modifiedAt = Instant.now(),
status = RequestStatus.TICKET_CREATED)
session.merge(update)
}
}
private fun getRevocationRequestEntity(requestId: String, status: RequestStatus? = null): CertificateRevocationRequestEntity? {
return database.transaction { return database.transaction {
uniqueEntityWhere { builder, path -> uniqueEntityWhere { builder, path ->
builder.equal(path.get<String>(CertificateRevocationRequestEntity::requestId.name), requestId) val idEqual = builder.equal(path.get<String>(CertificateRevocationRequestEntity::requestId.name), requestId)
if (status == null) {
idEqual
} else {
builder.and(idEqual, builder.equal(path.get<RequestStatus>(CertificateRevocationRequestEntity::status.name), status))
}
} }
} }
} }
@ -156,6 +175,8 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
private fun CertificateRevocationRequestEntity.toCertificateRevocationRequestData(): CertificateRevocationRequestData { private fun CertificateRevocationRequestEntity.toCertificateRevocationRequestData(): CertificateRevocationRequestData {
return CertificateRevocationRequestData( return CertificateRevocationRequestData(
requestId, requestId,
certificateData.certificateSigningRequest.requestId,
certificateData.certPath.certificates.first() as X509Certificate,
certificateSerialNumber, certificateSerialNumber,
modifiedAt, modifiedAt,
legalName, legalName,

View File

@ -0,0 +1,57 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package com.r3.corda.networkmanage.doorman
import com.atlassian.jira.rest.client.api.JiraRestClient
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestData
import net.corda.core.utilities.contextLogger
class CrrJiraClient(restClient: JiraRestClient, projectCode: String) : JiraClient(restClient, projectCode) {
companion object {
val logger = contextLogger()
}
fun createCertificateRevocationRequestTicket(revocationRequest: CertificateRevocationRequestData) {
// Check there isn't already a ticket for this request.
if (getIssueById(revocationRequest.requestId) != null) {
logger.warn("There is already a ticket corresponding to request Id ${revocationRequest.requestId}, not creating a new one.")
return
}
val ticketDescription = "Legal name: ${revocationRequest.legalName}\n" +
"Certificate serial number: ${revocationRequest.certificateSerialNumber}\n" +
"Revocation reason: ${revocationRequest.reason.name}\n" +
"Reporter: ${revocationRequest.reporter}\n" +
"CSR request ID: ${revocationRequest.certificateSigningRequestId}"
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
.setProjectKey(projectCode)
.setDescription(ticketDescription)
.setFieldValue(requestIdField.id, revocationRequest.requestId)
// This will block until the issue is created.
val issueId = restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim().key
val createdIssue = checkNotNull(getIssueById(issueId)) { "Missing the JIRA ticket for the request ID: $issueId" }
restClient.issueClient.addAttachment(createdIssue.attachmentsUri, revocationRequest.certificate.encoded.inputStream(), "${revocationRequest.certificateSerialNumber}.cer")
.fail { CsrJiraClient.logger.error("Error processing request '${createdIssue.key}' : Exception when uploading attachment to JIRA.", it) }.claim()
}
fun updateDoneCertificateRevocationRequests(doneRequests: List<String>) {
doneRequests.forEach { id ->
val issue = getIssueById(id)
issue ?: throw IllegalStateException("Missing the JIRA ticket for the request ID: $id")
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()
}
}
}

View File

@ -0,0 +1,93 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package com.r3.corda.networkmanage.doorman
import com.atlassian.jira.rest.client.api.JiraRestClient
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
import com.r3.corda.networkmanage.common.utils.getCertRole
import com.r3.corda.networkmanage.common.utils.getEmail
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.util.io.pem.PemObject
import java.io.StringWriter
import java.security.cert.CertPath
import javax.security.auth.x500.X500Principal
class CsrJiraClient(restClient: JiraRestClient, projectCode: String) : JiraClient(restClient, projectCode) {
companion object {
val logger = contextLogger()
}
// TODO: Pass in a parsed object instead of raw PKCS10 request.
fun createCertificateSigningRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) {
// Check there isn't already a ticket for this request.
if (getIssueById(requestId) != null) {
logger.warn("There is already a ticket corresponding to request Id $requestId, not creating a new one.")
return
}
// Make sure request has been accepted.
val request = StringWriter()
JcaPEMWriter(request).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", signingRequest.encoded))
}
// TODO The subject of the signing request has already been validated and parsed into a CordaX500Name. We shouldn't
// have to do it again here.
val subject = CordaX500Name.build(X500Principal(signingRequest.subject.encoded))
val email = signingRequest.getEmail()
val certRole = signingRequest.getCertRole()
val ticketSummary = if (subject.organisationUnit != null) {
"${subject.organisationUnit}, ${subject.organisation}"
} else {
subject.organisation
}
val data = mapOf("Requested Role Type" to certRole.name,
"Organisation" to subject.organisation,
"Organisation Unit" to subject.organisationUnit,
"Nearest City" to subject.locality,
"Country" to subject.country,
"Email" to email)
val ticketDescription = data.filter { it.value != null }.map { "${it.key}: ${it.value}" }.joinToString("\n") + "\n\n{code}$request{code}"
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
.setProjectKey(projectCode)
.setDescription(ticketDescription)
.setSummary(ticketSummary)
.setFieldValue(requestIdField.id, requestId)
// This will block until the issue is created.
restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
}
fun updateDoneCertificateSigningRequests(signedRequests: Map<String, CertPath>) {
// Retrieving certificates for signed CSRs to attach to the jira tasks.
signedRequests.forEach { (id, certPath) ->
val certificate = certPath.certificates.first()
val issue = getIssueById(id)
if (issue != null) {
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()
}
}
}
}

View File

@ -14,6 +14,7 @@ import com.google.common.primitives.Booleans
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.seconds import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import java.net.URL
import java.nio.file.Path import java.nio.file.Path
import java.util.* import java.util.*
@ -24,6 +25,7 @@ data class NetworkManagementServerConfig( // TODO: Move local signing to signing
val doorman: DoormanConfig?, val doorman: DoormanConfig?,
val networkMap: NetworkMapConfig?, val networkMap: NetworkMapConfig?,
val revocation: CertificateRevocationConfig?,
// TODO Should be part of a localSigning sub-config // TODO Should be part of a localSigning sub-config
val keystorePath: Path? = null, val keystorePath: Path? = null,
@ -54,6 +56,19 @@ data class DoormanConfig(val approveAll: Boolean = false,
} }
} }
data class CertificateRevocationConfig(val approveAll: Boolean = false,
val jira: JiraConfig? = null,
val crlUpdateInterval: Long,
val crlEndpoint: URL,
val crlCacheTimeout: Long,
val approveInterval: Long = NetworkManagementServerConfig.DEFAULT_APPROVE_INTERVAL.toMillis()) {
init {
require(Booleans.countTrue(approveAll, jira != null) == 1) {
"Either 'approveAll' or 'jira' config settings need to be specified but not both"
}
}
}
data class NetworkMapConfig(val cacheTimeout: Long, data class NetworkMapConfig(val cacheTimeout: Long,
// TODO: Move signing to signing server. // TODO: Move signing to signing server.
val signInterval: Long = NetworkManagementServerConfig.DEFAULT_SIGN_INTERVAL.toMillis()) val signInterval: Long = NetworkManagementServerConfig.DEFAULT_SIGN_INTERVAL.toMillis())

View File

@ -16,80 +16,23 @@ 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.TransitionInput import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
import com.r3.corda.networkmanage.common.utils.getCertRole
import com.r3.corda.networkmanage.common.utils.getEmail
import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.CertRole
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.util.io.pem.PemObject
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) { abstract class JiraClient(protected val restClient: JiraRestClient, protected val projectCode: String) {
companion object { companion object {
val logger = contextLogger() val logger = contextLogger()
} }
// The JIRA project must have a Request ID and reject reason 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'") protected 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'") protected 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'") protected val rejectReasonField: Field = restClient.metadataClient.fields.claim().find { it.name == "Reject Reason" } ?: throw IllegalArgumentException("Reject Reason field not found in JIRA '$projectCode'")
private var doneTransitionId: Int = -1 protected var doneTransitionId: Int = -1
private var canceledTransitionId: Int = -1 private var canceledTransitionId: Int = -1
private var startProgressTransitionId: Int = -1 private var startProgressTransitionId: Int = -1
// TODO: Pass in a parsed object instead of raw PKCS10 request.
fun createRequestTicket(requestId: String, signingRequest: PKCS10CertificationRequest) {
// Check there isn't already a ticket for this request.
if (getIssueById(requestId) != null) {
logger.warn("There is already a ticket corresponding to request Id $requestId, not creating a new one.")
return
}
// Make sure request has been accepted.
val request = StringWriter()
JcaPEMWriter(request).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", signingRequest.encoded))
}
// TODO The subject of the signing request has already been validated and parsed into a CordaX500Name. We shouldn't
// have to do it again here.
val subject = CordaX500Name.build(X500Principal(signingRequest.subject.encoded))
val email = signingRequest.getEmail()
val certRole = signingRequest.getCertRole()
val ticketSummary = if (subject.organisationUnit != null) {
"${subject.organisationUnit}, ${subject.organisation}"
} else {
subject.organisation
}
val data = mapOf("Requested Role Type" to certRole.name,
"Organisation" to subject.organisation,
"Organisation Unit" to subject.organisationUnit,
"Nearest City" to subject.locality,
"Country" to subject.country,
"Email" to email)
val ticketDescription = data.filter { it.value != null }.map { "${it.key}: ${it.value}" }.joinToString("\n") + "\n\n{code}$request{code}"
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
.setProjectKey(projectCode)
.setDescription(ticketDescription)
.setSummary(ticketSummary)
.setFieldValue(requestIdField.id, requestId)
// This will block until the issue is created.
restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
}
fun getApprovedRequests(): List<ApprovedRequest> { 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.mapNotNull { issue -> return issues.mapNotNull { issue ->
@ -113,22 +56,6 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode
} }
} }
fun updateSignedRequests(signedRequests: Map<String, CertPath>) {
// Retrieving certificates for signed CSRs to attach to the jira tasks.
signedRequests.forEach { (id, certPath) ->
val certificate = certPath.certificates.first()
val issue = getIssueById(id)
if (issue != null) {
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()
}
}
}
fun updateRejectedRequests(rejectedRequests: List<String>) { fun updateRejectedRequests(rejectedRequests: List<String>) {
rejectedRequests.mapNotNull { getIssueById(it) } rejectedRequests.mapNotNull { getIssueById(it) }
.forEach { issue -> .forEach { issue ->
@ -146,7 +73,7 @@ class JiraClient(private val restClient: JiraRestClient, private val projectCode
} }
} }
private fun getIssueById(requestId: String): Issue? { protected fun getIssueById(requestId: String): Issue? {
// Jira only support ~ (contains) search for custom textfield. // Jira only support ~ (contains) search for custom textfield.
return restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull() return restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
} }

View File

@ -100,6 +100,7 @@ private fun doormanMode(cmdLineOptions: DoormanCmdLineOptions, config: NetworkMa
config.address, config.address,
csrAndNetworkMap?.first, csrAndNetworkMap?.first,
config.doorman, config.doorman,
config.revocation,
networkMapStartParams) networkMapStartParams)
Runtime.getRuntime().addShutdownHook(object : Thread("ShutdownHook") { Runtime.getRuntime().addShutdownHook(object : Thread("ShutdownHook") {

View File

@ -14,16 +14,13 @@ import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientF
import com.r3.corda.networkmanage.common.persistence.* import com.r3.corda.networkmanage.common.persistence.*
import com.r3.corda.networkmanage.common.signer.NetworkMapSigner import com.r3.corda.networkmanage.common.signer.NetworkMapSigner
import com.r3.corda.networkmanage.common.utils.CertPathAndKey import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler import com.r3.corda.networkmanage.doorman.signer.*
import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler import com.r3.corda.networkmanage.doorman.webservice.*
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import com.r3.corda.networkmanage.doorman.webservice.MonitoringWebService
import com.r3.corda.networkmanage.doorman.webservice.NetworkMapWebService
import com.r3.corda.networkmanage.doorman.webservice.RegistrationWebService
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig import net.corda.nodeapi.internal.persistence.DatabaseConfig
import java.io.Closeable import java.io.Closeable
@ -48,7 +45,7 @@ class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig:
override fun close() { override fun close() {
logger.info("Closing server...") logger.info("Closing server...")
for (closeAction in closeActions) { for (closeAction in closeActions.reversed()) {
try { try {
closeAction() closeAction()
} catch (e: Exception) { } catch (e: Exception) {
@ -96,7 +93,7 @@ class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig:
val jiraConfig = config.jira val jiraConfig = config.jira
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) val jiraClient = CsrJiraClient(jiraWebAPI, jiraConfig.projectCode)
JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, csrCertPathAndKey)) JiraCsrHandler(jiraClient, requestService, DefaultCsrHandler(requestService, csrCertPathAndKey))
} else { } else {
DefaultCsrHandler(requestService, csrCertPathAndKey) DefaultCsrHandler(requestService, csrCertPathAndKey)
@ -119,9 +116,51 @@ class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig:
return RegistrationWebService(requestProcessor, Duration.ofMillis(config.approveInterval)) return RegistrationWebService(requestProcessor, Duration.ofMillis(config.approveInterval))
} }
private fun getRevocationServices(config: CertificateRevocationConfig,
database: CordaPersistence,
csrCertPathAndKeyPair: CertPathAndKey?): Pair<CertificateRevocationRequestWebService, CertificateRevocationListWebService> {
logger.info("Starting Revocation server.")
val crrStorage = if (config.approveAll) {
logger.warn("Revocation server is in 'Approve All' mode, this will approve all incoming certificate signing requests.")
ApproveAllCertificateRevocationRequestStorage(PersistentCertificateRevocationRequestStorage(database))
} else {
PersistentCertificateRevocationRequestStorage(database)
}
val crlStorage = PersistentCertificateRevocationListStorage(database)
val crlHandler = csrCertPathAndKeyPair?.let {
LocalCrlHandler(crrStorage, crlStorage, CertificateAndKeyPair(it.certPath.first(), it.toKeyPair()), Duration.ofMillis(config.crlUpdateInterval), config.crlEndpoint)
}
val jiraConfig = config.jira
val crrHandler = if (jiraConfig != null) {
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
val jiraClient = CrrJiraClient(jiraWebAPI, jiraConfig.projectCode)
JiraCrrHandler(jiraClient, crrStorage, crlHandler)
} else {
DefaultCrrHandler(crrStorage, crlHandler)
}
val scheduledExecutor = Executors.newScheduledThreadPool(1)
val approvalThread = Runnable {
try {
// Process Jira approved tickets.
crrHandler.processRequests()
} catch (e: Exception) {
// Log the error and carry on.
logger.error("Error encountered when approving request.", e)
}
}
scheduledExecutor.scheduleAtFixedRate(approvalThread, config.approveInterval, config.approveInterval, TimeUnit.MILLISECONDS)
closeActions += scheduledExecutor::shutdown
// TODO start socket server
return Pair(CertificateRevocationRequestWebService(crrHandler), CertificateRevocationListWebService(crlStorage, Duration.ofMillis(config.crlCacheTimeout)))
}
fun start(hostAndPort: NetworkHostAndPort, fun start(hostAndPort: NetworkHostAndPort,
csrCertPathAndKey: CertPathAndKey?, csrCertPathAndKey: CertPathAndKey?,
doormanConfig: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run doormanConfig: DoormanConfig?, // TODO Doorman config shouldn't be optional as the doorman is always required to run
revocationConfig: CertificateRevocationConfig?,
startNetworkMap: NetworkMapStartParams? startNetworkMap: NetworkMapStartParams?
) { ) {
val services = mutableListOf<Any>() val services = mutableListOf<Any>()
@ -129,6 +168,11 @@ class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig:
startNetworkMap?.let { services += getNetworkMapService(it.config, it.signer) } startNetworkMap?.let { services += getNetworkMapService(it.config, it.signer) }
doormanConfig?.let { services += getDoormanService(it, database, csrCertPathAndKey, serverStatus) } doormanConfig?.let { services += getDoormanService(it, database, csrCertPathAndKey, serverStatus) }
revocationConfig?.let {
val revocationServices = getRevocationServices(it, database, csrCertPathAndKey)
services += revocationServices.first
services += revocationServices.second
}
require(services.isNotEmpty()) { "No service created, please provide at least one service config." } require(services.isNotEmpty()) { "No service created, please provide at least one service config." }

View File

@ -0,0 +1,56 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package com.r3.corda.networkmanage.doorman.signer
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListStorage
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestStorage
import com.r3.corda.networkmanage.common.persistence.RequestStatus
import com.r3.corda.networkmanage.common.signer.CertificateRevocationListSigner
import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import java.net.URL
import java.time.Duration
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListStorage.Companion.DOORMAN_SIGNATURE
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.trace
class LocalCrlHandler(private val crrStorage: CertificateRevocationRequestStorage,
crlStorage: CertificateRevocationListStorage,
issuerCertAndKey: CertificateAndKeyPair,
crlUpdateInterval: Duration,
crlEndpoint: URL) {
private companion object {
private val logger = contextLogger()
}
private val crlSigner = CertificateRevocationListSigner(
crlStorage,
issuerCertAndKey.certificate,
crlUpdateInterval,
crlEndpoint,
LocalSigner(issuerCertAndKey))
fun signCrl() {
logger.info("Executing CRL signing...")
val approvedRequests = crrStorage.getRevocationRequests(RequestStatus.APPROVED)
logger.debug("Approved certificate revocation requests retrieved.")
logger.trace { approvedRequests.toString() }
if (approvedRequests.isEmpty()) {
// Nothing to add to the current CRL
logger.debug("There are no APPROVED certificate revocation requests. Aborting CRL signing.")
return
}
val currentRequests = crrStorage.getRevocationRequests(RequestStatus.DONE)
logger.debug("Existing certificate revocation requests retrieved.")
logger.trace { currentRequests.toString() }
crlSigner.createSignedCRL(approvedRequests, currentRequests, DOORMAN_SIGNATURE)
logger.info("New CRL signed.")
}
}

View File

@ -0,0 +1,27 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package com.r3.corda.networkmanage.doorman.signer
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestStorage
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
interface CrrHandler {
fun processRequests()
fun saveRevocationRequest(request: CertificateRevocationRequest): String
}
class DefaultCrrHandler(private val crrStorage: CertificateRevocationRequestStorage,
private val localCrlHandler: LocalCrlHandler?) : CrrHandler {
override fun saveRevocationRequest(request: CertificateRevocationRequest): String = crrStorage.saveRevocationRequest(request)
override fun processRequests() {
localCrlHandler?.signCrl()
}
}

View File

@ -0,0 +1,93 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package com.r3.corda.networkmanage.doorman.signer
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestData
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestStorage
import com.r3.corda.networkmanage.common.persistence.RequestStatus
import com.r3.corda.networkmanage.doorman.ApprovedRequest
import com.r3.corda.networkmanage.doorman.CrrJiraClient
import com.r3.corda.networkmanage.doorman.RejectedRequest
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
class JiraCrrHandler(private val jiraClient: CrrJiraClient,
private val crrStorage: CertificateRevocationRequestStorage,
private val localCrlHandler: LocalCrlHandler?) : CrrHandler {
private companion object {
val logger = contextLogger()
}
override fun saveRevocationRequest(request: CertificateRevocationRequest): String {
try {
val requestId = crrStorage.saveRevocationRequest(request)
val requestData = crrStorage.getRevocationRequest(requestId)
requestData ?: throw IllegalStateException("Request $requestId does not exist.")
jiraClient.createCertificateRevocationRequestTicket(requestData)
crrStorage.markRequestTicketCreated(requestId)
return requestId
} catch (e: Exception) {
logger.error("There was an error while creating JIRA tickets", e)
throw e
}
}
override fun processRequests() {
createTickets()
val (approvedRequests, rejectedRequests) = updateRequestStatuses()
updateJiraTickets(approvedRequests, rejectedRequests)
localCrlHandler?.signCrl()
}
private fun updateRequestStatuses(): Pair<List<ApprovedRequest>, List<RejectedRequest>> {
// Update local request statuses.
val approvedRequest = jiraClient.getApprovedRequests()
approvedRequest.forEach { (id, approvedBy) -> crrStorage.approveRevocationRequest(id, approvedBy) }
val rejectedRequest = jiraClient.getRejectedRequests()
rejectedRequest.forEach { (id, rejectedBy, reason) -> crrStorage.rejectRevocationRequest(id, rejectedBy, reason) }
return Pair(approvedRequest, rejectedRequest)
}
private fun updateJiraTickets(approvedRequest: List<ApprovedRequest>, rejectedRequest: List<RejectedRequest>) {
// Reconfirm request status and update jira status
val doneRequests = approvedRequest.mapNotNull { crrStorage.getRevocationRequest(it.requestId) }
.filter { it.status == RequestStatus.DONE }
.map { it.requestId }
jiraClient.updateDoneCertificateRevocationRequests(doneRequests)
val rejectedRequestIDs = rejectedRequest.mapNotNull { crrStorage.getRevocationRequest(it.requestId) }
.filter { it.status == RequestStatus.REJECTED }
.map { it.requestId }
jiraClient.updateRejectedRequests(rejectedRequestIDs)
}
/**
* Creates Jira tickets for all request in [RequestStatus.NEW] state.
*
* 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.
*/
private fun createTickets() {
crrStorage.getRevocationRequests(RequestStatus.NEW).forEach {
try {
createTicket(it)
} catch (e: Exception) {
logger.warn("There were errors while creating Jira tickets for request '${it.requestId}'", e)
}
}
}
private fun createTicket(revocationRequest: CertificateRevocationRequestData) {
jiraClient.createCertificateRevocationRequestTicket(revocationRequest)
crrStorage.markRequestTicketCreated(revocationRequest.requestId)
}
}

View File

@ -15,12 +15,12 @@ import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequest
import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequestStorage import com.r3.corda.networkmanage.common.persistence.CertificateSigningRequestStorage
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.ApprovedRequest
import com.r3.corda.networkmanage.doorman.JiraClient import com.r3.corda.networkmanage.doorman.CsrJiraClient
import com.r3.corda.networkmanage.doorman.RejectedRequest import com.r3.corda.networkmanage.doorman.RejectedRequest
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: CertificateSigningRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate { class JiraCsrHandler(private val jiraClient: CsrJiraClient, private val storage: CertificateSigningRequestStorage, private val delegate: CsrHandler) : CsrHandler by delegate {
private companion object { private companion object {
val log = contextLogger() val log = contextLogger()
} }
@ -30,7 +30,7 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce
// Make sure request has been accepted. // Make sure request has been accepted.
try { try {
if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) { if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) {
jiraClient.createRequestTicket(requestId, rawRequest) jiraClient.createCertificateSigningRequestTicket(requestId, rawRequest)
storage.markRequestTicketCreated(requestId) storage.markRequestTicketCreated(requestId)
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -62,7 +62,7 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce
.filter { it.status == RequestStatus.DONE && it.certData != null } .filter { it.status == RequestStatus.DONE && it.certData != null }
.associateBy { it.requestId } .associateBy { it.requestId }
.mapValues { it.value.certData!!.certPath } .mapValues { it.value.certData!!.certPath }
jiraClient.updateSignedRequests(signedRequests) jiraClient.updateDoneCertificateSigningRequests(signedRequests)
val rejectedRequestIDs = rejectedRequest.mapNotNull { storage.getRequest(it.requestId) } val rejectedRequestIDs = rejectedRequest.mapNotNull { storage.getRequest(it.requestId) }
.filter { it.status == RequestStatus.REJECTED } .filter { it.status == RequestStatus.REJECTED }
@ -87,7 +87,7 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce
} }
private fun createTicket(signingRequest: CertificateSigningRequest) { private fun createTicket(signingRequest: CertificateSigningRequest) {
jiraClient.createRequestTicket(signingRequest.requestId, signingRequest.request) jiraClient.createCertificateSigningRequestTicket(signingRequest.requestId, signingRequest.request)
storage.markRequestTicketCreated(signingRequest.requestId) storage.markRequestTicketCreated(signingRequest.requestId)
} }
} }

View File

@ -6,6 +6,7 @@ import com.r3.corda.networkmanage.common.persistence.CertificateRevocationListSt
import com.r3.corda.networkmanage.common.persistence.CrlIssuer import com.r3.corda.networkmanage.common.persistence.CrlIssuer
import com.r3.corda.networkmanage.doorman.webservice.CertificateRevocationListWebService.Companion.CRL_PATH import com.r3.corda.networkmanage.doorman.webservice.CertificateRevocationListWebService.Companion.CRL_PATH
import net.corda.core.utilities.contextLogger import net.corda.core.utilities.contextLogger
import java.time.Duration
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.ws.rs.GET import javax.ws.rs.GET
import javax.ws.rs.Path import javax.ws.rs.Path
@ -16,7 +17,7 @@ import javax.ws.rs.core.Response.status
@Path(CRL_PATH) @Path(CRL_PATH)
class CertificateRevocationListWebService(private val revocationListStorage: CertificateRevocationListStorage, class CertificateRevocationListWebService(private val revocationListStorage: CertificateRevocationListStorage,
cacheTimeout: Long) { cacheTimeout: Duration) {
companion object { companion object {
private val logger = contextLogger() private val logger = contextLogger()
const val CRL_PATH = "certificate-revocation-list" const val CRL_PATH = "certificate-revocation-list"
@ -26,7 +27,7 @@ class CertificateRevocationListWebService(private val revocationListStorage: Cer
} }
private val crlCache: LoadingCache<CrlIssuer, ByteArray> = Caffeine.newBuilder() private val crlCache: LoadingCache<CrlIssuer, ByteArray> = Caffeine.newBuilder()
.expireAfterWrite(cacheTimeout, TimeUnit.MILLISECONDS) .expireAfterWrite(cacheTimeout.toMillis(), TimeUnit.MILLISECONDS)
.build({ key -> .build({ key ->
revocationListStorage.getCertificateRevocationList(key)?.encoded revocationListStorage.getCertificateRevocationList(key)?.encoded
}) })

View File

@ -1,6 +1,6 @@
package com.r3.corda.networkmanage.doorman.webservice package com.r3.corda.networkmanage.doorman.webservice
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestStorage import com.r3.corda.networkmanage.doorman.signer.CrrHandler
import com.r3.corda.networkmanage.doorman.webservice.CertificateRevocationRequestWebService.Companion.CRR_PATH import com.r3.corda.networkmanage.doorman.webservice.CertificateRevocationRequestWebService.Companion.CRR_PATH
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.nodeapi.internal.network.CertificateRevocationRequest import net.corda.nodeapi.internal.network.CertificateRevocationRequest
@ -14,7 +14,7 @@ import javax.ws.rs.core.Response
import javax.ws.rs.core.Response.ok import javax.ws.rs.core.Response.ok
@Path(CRR_PATH) @Path(CRR_PATH)
class CertificateRevocationRequestWebService(private val revocationRequestStorage: CertificateRevocationRequestStorage) { class CertificateRevocationRequestWebService(private val crrHandler: CrrHandler) {
companion object { companion object {
const val CRR_PATH = "certificate-revocation-request" const val CRR_PATH = "certificate-revocation-request"
@ -25,7 +25,7 @@ class CertificateRevocationRequestWebService(private val revocationRequestStorag
@Produces(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN)
fun submitRequest(input: InputStream): Response { fun submitRequest(input: InputStream): Response {
val request = input.readBytes().deserialize<CertificateRevocationRequest>() val request = input.readBytes().deserialize<CertificateRevocationRequest>()
val requestId = revocationRequestStorage.saveRevocationRequest(request) val requestId = crrHandler.saveRevocationRequest(request)
return ok(requestId).build() return ok(requestId).build()
} }
} }

View File

@ -62,6 +62,8 @@ class CertificateRevocationListSignerTest : TestBase() {
private fun givenCertificateRevocationRequest(status: RequestStatus): CertificateRevocationRequestData { private fun givenCertificateRevocationRequest(status: RequestStatus): CertificateRevocationRequestData {
return CertificateRevocationRequestData( return CertificateRevocationRequestData(
SecureHash.randomSHA256().toString(), SecureHash.randomSHA256().toString(),
"CSR-ID-1",
mock(),
BigInteger.valueOf(random63BitValue()), BigInteger.valueOf(random63BitValue()),
Instant.now(), Instant.now(),
CordaX500Name.parse("CN=Bank A, O=$status, L=London, C=GB"), CordaX500Name.parse("CN=Bank A, O=$status, L=London, C=GB"),

View File

@ -23,18 +23,18 @@ import javax.security.auth.x500.X500Principal
@Ignore @Ignore
// This is manual test for testing Jira API. // This is manual test for testing Jira API.
class JiraClientTest { class CsrJiraClientTest {
private lateinit var jiraClient: JiraClient private lateinit var jiraClient: CsrJiraClient
@Before @Before
fun init() { fun init() {
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI("http://jira.url.com"), "username", "password") val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI("http://jira.url.com"), "username", "password")
jiraClient = JiraClient(jiraWebAPI, "DOOR") jiraClient = CsrJiraClient(jiraWebAPI, "DOOR")
} }
@Test @Test
fun createRequestTicket() { fun createRequestTicket() {
val request = X509Utilities.createCertificateSigningRequest(CordaX500Name("JiraAPITest", "R3 Ltd 3", "London", "GB").x500Principal, "test@test.com", Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) 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) jiraClient.createCertificateSigningRequestTicket(SecureHash.randomSHA256().toString(), request)
} }
@Test @Test
@ -54,7 +54,7 @@ class JiraClientTest {
val selfSignedCaCertPath = X509Utilities.buildCertPath(X509Utilities.createSelfSignedCACertificate( val selfSignedCaCertPath = X509Utilities.buildCertPath(X509Utilities.createSelfSignedCACertificate(
X500Principal("O=test,L=london,C=GB"), X500Principal("O=test,L=london,C=GB"),
Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))) Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)))
jiraClient.updateSignedRequests(requests.associateBy({ it.requestId }, { selfSignedCaCertPath })) jiraClient.updateDoneCertificateSigningRequests(requests.associateBy({ it.requestId }, { selfSignedCaCertPath }))
} }
@Test @Test

View File

@ -14,14 +14,12 @@ import com.nhaarman.mockito_kotlin.*
import com.r3.corda.networkmanage.TestBase import com.r3.corda.networkmanage.TestBase
import com.r3.corda.networkmanage.common.persistence.* import com.r3.corda.networkmanage.common.persistence.*
import com.r3.corda.networkmanage.doorman.ApprovedRequest import com.r3.corda.networkmanage.doorman.ApprovedRequest
import com.r3.corda.networkmanage.doorman.JiraClient import com.r3.corda.networkmanage.doorman.CsrJiraClient
import com.r3.corda.networkmanage.doorman.RejectedRequest 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.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 net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@ -37,7 +35,7 @@ class JiraCsrHandlerTest : TestBase() {
val mockitoRule: MockitoRule = MockitoJUnit.rule() val mockitoRule: MockitoRule = MockitoJUnit.rule()
@Mock @Mock
private lateinit var jiraClient: JiraClient private lateinit var jiraClient: CsrJiraClient
@Mock @Mock
private lateinit var certificationRequestStorage: CertificateSigningRequestStorage private lateinit var certificationRequestStorage: CertificateSigningRequestStorage
@ -68,7 +66,7 @@ class JiraCsrHandlerTest : TestBase() {
fun `If jira connection fails we don't mark the ticket as created`() { fun `If jira connection fails we don't mark the ticket as created`() {
whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId) whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse) whenever(defaultCsrHandler.getResponse(requestId)).thenReturn(certificateResponse)
whenever(jiraClient.createRequestTicket(eq(requestId), any())).thenThrow(IllegalStateException("something broke")) whenever(jiraClient.createCertificateSigningRequestTicket(eq(requestId), any())).thenThrow(IllegalStateException("something broke"))
// Test // Test
jiraCsrHandler.saveRequest(pkcS10CertificationRequest) jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
@ -91,7 +89,7 @@ class JiraCsrHandlerTest : TestBase() {
fun `create tickets`() { fun `create tickets`() {
val csr = certificateSigningRequest( val csr = certificateSigningRequest(
requestId = requestId, requestId = requestId,
legalName = ALICE_NAME, legalName = CordaX500Name.parse("O=Test Org., C=GB, L=London"),
status = RequestStatus.NEW, status = RequestStatus.NEW,
request = pkcS10CertificationRequest) request = pkcS10CertificationRequest)
whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr)) whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr))
@ -99,7 +97,7 @@ class JiraCsrHandlerTest : TestBase() {
// Test // Test
jiraCsrHandler.processRequests() jiraCsrHandler.processRequests()
verify(jiraClient).createRequestTicket(requestId, csr.request) verify(jiraClient).createCertificateSigningRequestTicket(requestId, csr.request)
verify(certificationRequestStorage).markRequestTicketCreated(requestId) verify(certificationRequestStorage).markRequestTicketCreated(requestId)
} }
@ -107,8 +105,8 @@ class JiraCsrHandlerTest : TestBase() {
fun `sync tickets status`() { fun `sync tickets status`() {
val id1 = SecureHash.randomSHA256().toString() val id1 = SecureHash.randomSHA256().toString()
val id2 = SecureHash.randomSHA256().toString() val id2 = SecureHash.randomSHA256().toString()
val csr1 = CertificateSigningRequest(id1, ALICE_NAME, SecureHash.randomSHA256(), RequestStatus.NEW, pkcS10CertificationRequest, null, "Test", null) val csr1 = CertificateSigningRequest(id1, CordaX500Name.parse("O=Test1 Org., C=GB, L=London"), SecureHash.randomSHA256(), RequestStatus.NEW, pkcS10CertificationRequest, null, "Test", null)
val csr2 = CertificateSigningRequest(id2, BOB_NAME, SecureHash.randomSHA256(), RequestStatus.NEW, pkcS10CertificationRequest, null, "Test", null) val csr2 = CertificateSigningRequest(id2, CordaX500Name.parse("O=Test2 Org., C=GB, L=London"), SecureHash.randomSHA256(), RequestStatus.NEW, pkcS10CertificationRequest, null, "Test", null)
val requests = mutableMapOf(id1 to csr1, id2 to csr2) val requests = mutableMapOf(id1 to csr1, id2 to csr2)
@ -135,8 +133,8 @@ class JiraCsrHandlerTest : TestBase() {
// Test. // Test.
jiraCsrHandler.processRequests() jiraCsrHandler.processRequests()
verify(jiraClient).createRequestTicket(id1, csr1.request) verify(jiraClient).createCertificateSigningRequestTicket(id1, csr1.request)
verify(jiraClient).createRequestTicket(id2, csr2.request) verify(jiraClient).createCertificateSigningRequestTicket(id2, csr2.request)
verify(certificationRequestStorage).markRequestTicketCreated(id1) verify(certificationRequestStorage).markRequestTicketCreated(id1)
verify(certificationRequestStorage).markRequestTicketCreated(id2) verify(certificationRequestStorage).markRequestTicketCreated(id2)
@ -147,7 +145,7 @@ class JiraCsrHandlerTest : TestBase() {
// Verify jira client get the correct call. // Verify jira client get the correct call.
verify(jiraClient).updateRejectedRequests(listOf(id2)) verify(jiraClient).updateRejectedRequests(listOf(id2))
verify(jiraClient).updateSignedRequests(emptyMap()) verify(jiraClient).updateDoneCertificateSigningRequests(emptyMap())
// Sign request 1 // Sign request 1
val certPath = mock<CertPath>() val certPath = mock<CertPath>()
@ -158,6 +156,6 @@ class JiraCsrHandlerTest : TestBase() {
jiraCsrHandler.processRequests() jiraCsrHandler.processRequests()
// Update signed request should be called. // Update signed request should be called.
verify(jiraClient).updateSignedRequests(mapOf(id1 to certPath)) verify(jiraClient).updateDoneCertificateSigningRequests(mapOf(id1 to certPath))
} }
} }

View File

@ -11,6 +11,7 @@ import com.r3.corda.networkmanage.common.utils.writeObject
import net.corda.core.crypto.Crypto import net.corda.core.crypto.Crypto
import net.corda.core.identity.CordaX500Name import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.x500Name import net.corda.core.internal.x500Name
import net.corda.nodeapi.internal.createDevNodeCa
import net.corda.nodeapi.internal.crypto.ContentSignerBuilder import net.corda.nodeapi.internal.crypto.ContentSignerBuilder
import net.corda.testing.core.SerializationEnvironmentRule import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.freePort import net.corda.testing.core.freePort
@ -117,7 +118,9 @@ class CertificateRevocationSocketServerTest {
certificateSerialNumber = BigInteger.TEN, certificateSerialNumber = BigInteger.TEN,
status = status, status = status,
reason = CRLReason.KEY_COMPROMISE, reason = CRLReason.KEY_COMPROMISE,
modifiedAt = Instant.now() modifiedAt = Instant.now(),
certificateSigningRequestId = "CSR-ID",
certificate = createDevNodeCa(DEV_INTERMEDIATE_CA, CordaX500Name.parse("O=R3Cev, L=London, C=GB")).certificate
) )
} }
} }