mirror of
https://github.com/corda/corda.git
synced 2025-01-15 09:20:22 +00:00
Adding jira integration to CRL (#579)
* Adding jira integration * Addressing review comments * Adding review comments * Fixes after rebase
This commit is contained in:
parent
c9caaf8be4
commit
101eee1ff5
@ -146,6 +146,7 @@ class NetworkParametersUpdateTest : IntegrationTest() {
|
||||
serverAddress,
|
||||
CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private),
|
||||
DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis),
|
||||
null,
|
||||
if (startNetworkMap) {
|
||||
NetworkMapStartParams(
|
||||
LocalSigner(networkMapCa),
|
||||
|
@ -159,6 +159,13 @@ class NodeRegistrationTest : IntegrationTest() {
|
||||
serverAddress,
|
||||
CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private),
|
||||
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) {
|
||||
NetworkMapStartParams(
|
||||
LocalSigner(networkMapCa),
|
||||
|
@ -14,6 +14,7 @@ import com.nhaarman.mockito_kotlin.*
|
||||
import com.r3.corda.networkmanage.common.HOST
|
||||
import com.r3.corda.networkmanage.common.HsmBaseTest
|
||||
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.NetworkManagementServer
|
||||
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.uncheckedCast
|
||||
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.node.NodeRegistrationOption
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
@ -101,6 +104,14 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
|
||||
hostAndPort = NetworkHostAndPort(HOST, 0),
|
||||
csrCertPathAndKey = 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)
|
||||
val doormanHostAndPort = server.hostAndPort
|
||||
// Start Corda network registration.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -7,6 +7,10 @@ import java.time.Instant
|
||||
* Interface for managing certificate revocation list persistence
|
||||
*/
|
||||
interface CertificateRevocationListStorage {
|
||||
companion object {
|
||||
val DOORMAN_SIGNATURE = "Doorman-Crl-Signer"
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the latest certificate revocation list.
|
||||
*
|
||||
|
@ -5,6 +5,7 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
|
||||
import java.math.BigInteger
|
||||
import java.security.cert.CRLReason
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
@ -12,6 +13,8 @@ import java.time.Instant
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class CertificateRevocationRequestData(val requestId: String, // This is a uniquely generated string
|
||||
val certificateSigningRequestId: String,
|
||||
val certificate: X509Certificate,
|
||||
val certificateSerialNumber: BigInteger,
|
||||
val modifiedAt: Instant,
|
||||
val legalName: CordaX500Name,
|
||||
@ -23,6 +26,9 @@ data class CertificateRevocationRequestData(val requestId: String, // This is a
|
||||
* Interface for managing certificate revocation requests persistence
|
||||
*/
|
||||
interface CertificateRevocationRequestStorage {
|
||||
companion object {
|
||||
val DOORMAN_SIGNATURE = "Doorman-Crr-Signer"
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new revocation request for the given [certificateSerialNumber].
|
||||
@ -70,5 +76,10 @@ interface CertificateRevocationRequestStorage {
|
||||
* @param rejectedBy who is rejecting it
|
||||
* @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)
|
||||
}
|
@ -32,7 +32,7 @@ data class CertificateSigningRequest(val requestId: String,
|
||||
*/
|
||||
interface CertificateSigningRequestStorage {
|
||||
companion object {
|
||||
val DOORMAN_SIGNATURE = "Doorman"
|
||||
val DOORMAN_SIGNATURE = "Doorman-Csr-Signer"
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,6 +10,7 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
|
||||
import net.corda.nodeapi.internal.persistence.TransactionIsolationLevel
|
||||
import java.math.BigInteger
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
|
||||
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 {
|
||||
val revocation = getRevocationRequestEntity(requestId)
|
||||
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 {
|
||||
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 {
|
||||
return CertificateRevocationRequestData(
|
||||
requestId,
|
||||
certificateData.certificateSigningRequest.requestId,
|
||||
certificateData.certPath.certificates.first() as X509Certificate,
|
||||
certificateSerialNumber,
|
||||
modifiedAt,
|
||||
legalName,
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ import com.google.common.primitives.Booleans
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import java.net.URL
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
|
||||
@ -24,6 +25,7 @@ data class NetworkManagementServerConfig( // TODO: Move local signing to signing
|
||||
|
||||
val doorman: DoormanConfig?,
|
||||
val networkMap: NetworkMapConfig?,
|
||||
val revocation: CertificateRevocationConfig?,
|
||||
|
||||
// TODO Should be part of a localSigning sub-config
|
||||
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,
|
||||
// TODO: Move signing to signing server.
|
||||
val signInterval: Long = NetworkManagementServerConfig.DEFAULT_SIGN_INTERVAL.toMillis())
|
||||
|
@ -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.Issue
|
||||
import com.atlassian.jira.rest.client.api.domain.IssueType
|
||||
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
|
||||
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
|
||||
import 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.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 {
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
// The JIRA project must have a Request ID and reject reason field, and the Task issue type.
|
||||
private val requestIdField: Field = restClient.metadataClient.fields.claim().find { it.name == "Request ID" } ?: throw IllegalArgumentException("Request ID field not found in JIRA '$projectCode'")
|
||||
private val rejectReasonField: Field = restClient.metadataClient.fields.claim().find { it.name == "Reject Reason" } ?: throw IllegalArgumentException("Reject Reason field not found in JIRA '$projectCode'")
|
||||
private val taskIssueType: IssueType = restClient.metadataClient.issueTypes.claim().find { it.name == "Task" } ?: throw IllegalArgumentException("Task issue type field not found in JIRA '$projectCode'")
|
||||
protected val requestIdField: Field = restClient.metadataClient.fields.claim().find { it.name == "Request ID" } ?: throw IllegalArgumentException("Request ID 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'")
|
||||
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 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> {
|
||||
val issues = restClient.searchClient.searchJql("project = $projectCode AND status = Approved").claim().issues
|
||||
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>) {
|
||||
rejectedRequests.mapNotNull { getIssueById(it) }
|
||||
.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.
|
||||
return restClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
|
||||
}
|
||||
|
@ -100,6 +100,7 @@ private fun doormanMode(cmdLineOptions: DoormanCmdLineOptions, config: NetworkMa
|
||||
config.address,
|
||||
csrAndNetworkMap?.first,
|
||||
config.doorman,
|
||||
config.revocation,
|
||||
networkMapStartParams)
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(object : Thread("ShutdownHook") {
|
||||
|
@ -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.signer.NetworkMapSigner
|
||||
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
|
||||
import com.r3.corda.networkmanage.doorman.signer.DefaultCsrHandler
|
||||
import com.r3.corda.networkmanage.doorman.signer.JiraCsrHandler
|
||||
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 com.r3.corda.networkmanage.doorman.signer.*
|
||||
import com.r3.corda.networkmanage.doorman.webservice.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
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.DatabaseConfig
|
||||
import java.io.Closeable
|
||||
@ -48,7 +45,7 @@ class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig:
|
||||
|
||||
override fun close() {
|
||||
logger.info("Closing server...")
|
||||
for (closeAction in closeActions) {
|
||||
for (closeAction in closeActions.reversed()) {
|
||||
try {
|
||||
closeAction()
|
||||
} catch (e: Exception) {
|
||||
@ -96,7 +93,7 @@ class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig:
|
||||
val jiraConfig = config.jira
|
||||
val requestProcessor = if (jiraConfig != null) {
|
||||
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))
|
||||
} else {
|
||||
DefaultCsrHandler(requestService, csrCertPathAndKey)
|
||||
@ -119,9 +116,51 @@ class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig:
|
||||
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,
|
||||
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?
|
||||
) {
|
||||
val services = mutableListOf<Any>()
|
||||
@ -129,6 +168,11 @@ class NetworkManagementServer(dataSourceProperties: Properties, databaseConfig:
|
||||
|
||||
startNetworkMap?.let { services += getNetworkMapService(it.config, it.signer) }
|
||||
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." }
|
||||
|
||||
|
@ -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.")
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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.RequestStatus
|
||||
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 net.corda.core.utilities.contextLogger
|
||||
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 {
|
||||
val log = contextLogger()
|
||||
}
|
||||
@ -30,7 +30,7 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce
|
||||
// Make sure request has been accepted.
|
||||
try {
|
||||
if (delegate.getResponse(requestId) !is CertificateResponse.Unauthorised) {
|
||||
jiraClient.createRequestTicket(requestId, rawRequest)
|
||||
jiraClient.createCertificateSigningRequestTicket(requestId, rawRequest)
|
||||
storage.markRequestTicketCreated(requestId)
|
||||
}
|
||||
} 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 }
|
||||
.associateBy { it.requestId }
|
||||
.mapValues { it.value.certData!!.certPath }
|
||||
jiraClient.updateSignedRequests(signedRequests)
|
||||
jiraClient.updateDoneCertificateSigningRequests(signedRequests)
|
||||
|
||||
val rejectedRequestIDs = rejectedRequest.mapNotNull { storage.getRequest(it.requestId) }
|
||||
.filter { it.status == RequestStatus.REJECTED }
|
||||
@ -87,7 +87,7 @@ class JiraCsrHandler(private val jiraClient: JiraClient, private val storage: Ce
|
||||
}
|
||||
|
||||
private fun createTicket(signingRequest: CertificateSigningRequest) {
|
||||
jiraClient.createRequestTicket(signingRequest.requestId, signingRequest.request)
|
||||
jiraClient.createCertificateSigningRequestTicket(signingRequest.requestId, signingRequest.request)
|
||||
storage.markRequestTicketCreated(signingRequest.requestId)
|
||||
}
|
||||
}
|
||||
|
@ -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.doorman.webservice.CertificateRevocationListWebService.Companion.CRL_PATH
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.ws.rs.GET
|
||||
import javax.ws.rs.Path
|
||||
@ -16,7 +17,7 @@ import javax.ws.rs.core.Response.status
|
||||
|
||||
@Path(CRL_PATH)
|
||||
class CertificateRevocationListWebService(private val revocationListStorage: CertificateRevocationListStorage,
|
||||
cacheTimeout: Long) {
|
||||
cacheTimeout: Duration) {
|
||||
companion object {
|
||||
private val logger = contextLogger()
|
||||
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()
|
||||
.expireAfterWrite(cacheTimeout, TimeUnit.MILLISECONDS)
|
||||
.expireAfterWrite(cacheTimeout.toMillis(), TimeUnit.MILLISECONDS)
|
||||
.build({ key ->
|
||||
revocationListStorage.getCertificateRevocationList(key)?.encoded
|
||||
})
|
||||
|
@ -1,6 +1,6 @@
|
||||
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 net.corda.core.serialization.deserialize
|
||||
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
|
||||
@ -14,7 +14,7 @@ import javax.ws.rs.core.Response
|
||||
import javax.ws.rs.core.Response.ok
|
||||
|
||||
@Path(CRR_PATH)
|
||||
class CertificateRevocationRequestWebService(private val revocationRequestStorage: CertificateRevocationRequestStorage) {
|
||||
class CertificateRevocationRequestWebService(private val crrHandler: CrrHandler) {
|
||||
|
||||
companion object {
|
||||
const val CRR_PATH = "certificate-revocation-request"
|
||||
@ -25,7 +25,7 @@ class CertificateRevocationRequestWebService(private val revocationRequestStorag
|
||||
@Produces(MediaType.TEXT_PLAIN)
|
||||
fun submitRequest(input: InputStream): Response {
|
||||
val request = input.readBytes().deserialize<CertificateRevocationRequest>()
|
||||
val requestId = revocationRequestStorage.saveRevocationRequest(request)
|
||||
val requestId = crrHandler.saveRevocationRequest(request)
|
||||
return ok(requestId).build()
|
||||
}
|
||||
}
|
@ -62,6 +62,8 @@ class CertificateRevocationListSignerTest : TestBase() {
|
||||
private fun givenCertificateRevocationRequest(status: RequestStatus): CertificateRevocationRequestData {
|
||||
return CertificateRevocationRequestData(
|
||||
SecureHash.randomSHA256().toString(),
|
||||
"CSR-ID-1",
|
||||
mock(),
|
||||
BigInteger.valueOf(random63BitValue()),
|
||||
Instant.now(),
|
||||
CordaX500Name.parse("CN=Bank A, O=$status, L=London, C=GB"),
|
||||
|
@ -23,18 +23,18 @@ import javax.security.auth.x500.X500Principal
|
||||
|
||||
@Ignore
|
||||
// This is manual test for testing Jira API.
|
||||
class JiraClientTest {
|
||||
private lateinit var jiraClient: JiraClient
|
||||
class CsrJiraClientTest {
|
||||
private lateinit var jiraClient: CsrJiraClient
|
||||
@Before
|
||||
fun init() {
|
||||
val jiraWebAPI = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI("http://jira.url.com"), "username", "password")
|
||||
jiraClient = JiraClient(jiraWebAPI, "DOOR")
|
||||
jiraClient = CsrJiraClient(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)
|
||||
jiraClient.createCertificateSigningRequestTicket(SecureHash.randomSHA256().toString(), request)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -54,7 +54,7 @@ class JiraClientTest {
|
||||
val selfSignedCaCertPath = X509Utilities.buildCertPath(X509Utilities.createSelfSignedCACertificate(
|
||||
X500Principal("O=test,L=london,C=GB"),
|
||||
Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)))
|
||||
jiraClient.updateSignedRequests(requests.associateBy({ it.requestId }, { selfSignedCaCertPath }))
|
||||
jiraClient.updateDoneCertificateSigningRequests(requests.associateBy({ it.requestId }, { selfSignedCaCertPath }))
|
||||
}
|
||||
|
||||
@Test
|
@ -14,14 +14,12 @@ import com.nhaarman.mockito_kotlin.*
|
||||
import com.r3.corda.networkmanage.TestBase
|
||||
import com.r3.corda.networkmanage.common.persistence.*
|
||||
import com.r3.corda.networkmanage.doorman.ApprovedRequest
|
||||
import com.r3.corda.networkmanage.doorman.JiraClient
|
||||
import com.r3.corda.networkmanage.doorman.CsrJiraClient
|
||||
import com.r3.corda.networkmanage.doorman.RejectedRequest
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.nodeapi.internal.crypto.X509Utilities
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@ -37,7 +35,7 @@ class JiraCsrHandlerTest : TestBase() {
|
||||
val mockitoRule: MockitoRule = MockitoJUnit.rule()
|
||||
|
||||
@Mock
|
||||
private lateinit var jiraClient: JiraClient
|
||||
private lateinit var jiraClient: CsrJiraClient
|
||||
|
||||
@Mock
|
||||
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`() {
|
||||
whenever(defaultCsrHandler.saveRequest(any())).thenReturn(requestId)
|
||||
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
|
||||
jiraCsrHandler.saveRequest(pkcS10CertificationRequest)
|
||||
@ -91,7 +89,7 @@ class JiraCsrHandlerTest : TestBase() {
|
||||
fun `create tickets`() {
|
||||
val csr = certificateSigningRequest(
|
||||
requestId = requestId,
|
||||
legalName = ALICE_NAME,
|
||||
legalName = CordaX500Name.parse("O=Test Org., C=GB, L=London"),
|
||||
status = RequestStatus.NEW,
|
||||
request = pkcS10CertificationRequest)
|
||||
whenever(certificationRequestStorage.getRequests(RequestStatus.NEW)).thenReturn(listOf(csr))
|
||||
@ -99,7 +97,7 @@ class JiraCsrHandlerTest : TestBase() {
|
||||
// Test
|
||||
jiraCsrHandler.processRequests()
|
||||
|
||||
verify(jiraClient).createRequestTicket(requestId, csr.request)
|
||||
verify(jiraClient).createCertificateSigningRequestTicket(requestId, csr.request)
|
||||
verify(certificationRequestStorage).markRequestTicketCreated(requestId)
|
||||
}
|
||||
|
||||
@ -107,8 +105,8 @@ class JiraCsrHandlerTest : TestBase() {
|
||||
fun `sync tickets status`() {
|
||||
val id1 = SecureHash.randomSHA256().toString()
|
||||
val id2 = SecureHash.randomSHA256().toString()
|
||||
val csr1 = CertificateSigningRequest(id1, ALICE_NAME, SecureHash.randomSHA256(), RequestStatus.NEW, pkcS10CertificationRequest, null, "Test", null)
|
||||
val csr2 = CertificateSigningRequest(id2, BOB_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, 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)
|
||||
|
||||
@ -135,8 +133,8 @@ class JiraCsrHandlerTest : TestBase() {
|
||||
// Test.
|
||||
jiraCsrHandler.processRequests()
|
||||
|
||||
verify(jiraClient).createRequestTicket(id1, csr1.request)
|
||||
verify(jiraClient).createRequestTicket(id2, csr2.request)
|
||||
verify(jiraClient).createCertificateSigningRequestTicket(id1, csr1.request)
|
||||
verify(jiraClient).createCertificateSigningRequestTicket(id2, csr2.request)
|
||||
|
||||
verify(certificationRequestStorage).markRequestTicketCreated(id1)
|
||||
verify(certificationRequestStorage).markRequestTicketCreated(id2)
|
||||
@ -147,7 +145,7 @@ class JiraCsrHandlerTest : TestBase() {
|
||||
|
||||
// Verify jira client get the correct call.
|
||||
verify(jiraClient).updateRejectedRequests(listOf(id2))
|
||||
verify(jiraClient).updateSignedRequests(emptyMap())
|
||||
verify(jiraClient).updateDoneCertificateSigningRequests(emptyMap())
|
||||
|
||||
// Sign request 1
|
||||
val certPath = mock<CertPath>()
|
||||
@ -158,6 +156,6 @@ class JiraCsrHandlerTest : TestBase() {
|
||||
jiraCsrHandler.processRequests()
|
||||
|
||||
// Update signed request should be called.
|
||||
verify(jiraClient).updateSignedRequests(mapOf(id1 to certPath))
|
||||
verify(jiraClient).updateDoneCertificateSigningRequests(mapOf(id1 to certPath))
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import com.r3.corda.networkmanage.common.utils.writeObject
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.x500Name
|
||||
import net.corda.nodeapi.internal.createDevNodeCa
|
||||
import net.corda.nodeapi.internal.crypto.ContentSignerBuilder
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.freePort
|
||||
@ -117,7 +118,9 @@ class CertificateRevocationSocketServerTest {
|
||||
certificateSerialNumber = BigInteger.TEN,
|
||||
status = status,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user