Doorman-Jira integration

This commit is contained in:
Patrick Kuo 2017-02-15 15:42:41 +00:00
parent 7cd8d952a9
commit 234863f88c
9 changed files with 126 additions and 28 deletions

View File

@ -68,4 +68,11 @@ dependencies {
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile "org.assertj:assertj-core:${assertj_version}" testCompile "org.assertj:assertj-core:${assertj_version}"
testCompile "com.nhaarman:mockito-kotlin:0.6.1" testCompile "com.nhaarman:mockito-kotlin:0.6.1"
compile ('com.atlassian.jira:jira-rest-java-client-core:4.0.0'){
// The jira client includes jersey-core 1.5 which breaks everything.
exclude module: 'jersey-core'
}
// Needed by jira rest client
compile "com.atlassian.fugue:fugue:2.6.1"
} }

View File

@ -1,6 +1,7 @@
package com.r3.corda.doorman package com.r3.corda.doorman
import com.r3.corda.doorman.OptionParserHelper.toConfigWithOptions import com.r3.corda.doorman.OptionParserHelper.toConfigWithOptions
import com.typesafe.config.Config
import net.corda.core.div import net.corda.core.div
import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.getOrElse import net.corda.node.services.config.getOrElse
@ -36,6 +37,7 @@ class DoormanParameters(args: Array<String>) {
val host: String by config val host: String by config
val port: Int by config val port: Int by config
val dataSourceProperties: Properties by config val dataSourceProperties: Properties by config
val jiraConfig = if (config.hasPath("jiraConfig")) JiraConfig(config.getConfig("jiraConfig")) else null
private val keygen: Boolean by config.getOrElse { false } private val keygen: Boolean by config.getOrElse { false }
private val rootKeygen: Boolean by config.getOrElse { false } private val rootKeygen: Boolean by config.getOrElse { false }
@ -44,6 +46,12 @@ class DoormanParameters(args: Array<String>) {
enum class Mode { enum class Mode {
DOORMAN, CA_KEYGEN, ROOT_KEYGEN DOORMAN, CA_KEYGEN, ROOT_KEYGEN
} }
class JiraConfig(config: Config) {
val address: String by config
val projectCode: String by config
val username: String by config
val password: String by config
val doneTransitionCode: Int by config
}
} }

View File

@ -1,9 +1,11 @@
package com.r3.corda.doorman package com.r3.corda.doorman
import com.atlassian.jira.rest.client.internal.async.AsynchronousJiraRestClientFactory
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import com.r3.corda.doorman.DoormanServer.Companion.logger import com.r3.corda.doorman.DoormanServer.Companion.logger
import com.r3.corda.doorman.persistence.CertificationRequestStorage import com.r3.corda.doorman.persistence.CertificationRequestStorage
import com.r3.corda.doorman.persistence.DBCertificateRequestStorage import com.r3.corda.doorman.persistence.DBCertificateRequestStorage
import com.r3.corda.doorman.persistence.JiraCertificateRequestStorage
import net.corda.core.createDirectories import net.corda.core.createDirectories
import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.X509Utilities.CACertAndKey import net.corda.core.crypto.X509Utilities.CACertAndKey
@ -13,6 +15,7 @@ import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA
import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY
import net.corda.core.crypto.X509Utilities.addOrReplaceKey import net.corda.core.crypto.X509Utilities.addOrReplaceKey
import net.corda.core.crypto.X509Utilities.createIntermediateCert import net.corda.core.crypto.X509Utilities.createIntermediateCert
import net.corda.core.crypto.X509Utilities.createServerCert
import net.corda.core.crypto.X509Utilities.loadCertificateAndKey import net.corda.core.crypto.X509Utilities.loadCertificateAndKey
import net.corda.core.crypto.X509Utilities.loadKeyStore import net.corda.core.crypto.X509Utilities.loadKeyStore
import net.corda.core.crypto.X509Utilities.loadOrCreateKeyStore import net.corda.core.crypto.X509Utilities.loadOrCreateKeyStore
@ -31,6 +34,7 @@ import org.glassfish.jersey.servlet.ServletContainer
import java.io.Closeable import java.io.Closeable
import java.lang.Thread.sleep import java.lang.Thread.sleep
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.URI
import java.security.cert.Certificate import java.security.cert.Certificate
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.system.exitProcess import kotlin.system.exitProcess
@ -159,25 +163,31 @@ private fun DoormanParameters.startDoorman() {
val caCertAndKey = X509Utilities.loadCertificateAndKey(keystore, caPrivateKeyPassword, CORDA_INTERMEDIATE_CA_PRIVATE_KEY) val caCertAndKey = X509Utilities.loadCertificateAndKey(keystore, caPrivateKeyPassword, CORDA_INTERMEDIATE_CA_PRIVATE_KEY)
// Create DB connection. // Create DB connection.
val (datasource, database) = configureDatabase(dataSourceProperties) val (datasource, database) = configureDatabase(dataSourceProperties)
val storage = DBCertificateRequestStorage(database)
// Daemon thread approving all request periodically. val storage = if (approveAll) {
if (approveAll) {
thread(name = "Request Approval Daemon", isDaemon = true) {
logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing request.") logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing request.")
DBCertificateRequestStorage(database)
} else {
// Require JIRA config to be non-null.
val jiraClient = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig!!.address), jiraConfig.username, jiraConfig.password)
JiraCertificateRequestStorage(DBCertificateRequestStorage(database), jiraClient, jiraConfig.projectCode, jiraConfig.doneTransitionCode)
}
// Daemon thread approving request periodically.
thread(name = "Request Approval Daemon") {
while (true) { while (true) {
sleep(10.seconds.toMillis()) sleep(10.seconds.toMillis())
for (id in storage.getPendingRequestIds()) { val approvedRequests = (storage as? JiraCertificateRequestStorage)?.getRequestByStatus(JiraCertificateRequestStorage.APPROVED) ?: storage.getPendingRequestIds()
storage.approveRequest(id, { for (id in approvedRequests) {
JcaPKCS10CertificationRequest(it.request).run { storage.approveRequest(id) {
X509Utilities.createServerCert(subject, publicKey, caCertAndKey, val request = JcaPKCS10CertificationRequest(request)
if (it.ipAddress == it.hostName) listOf() else listOf(it.hostName), listOf(it.ipAddress)) createServerCert(request.subject, request.publicKey, caCertAndKey,
if (ipAddress == hostName) listOf() else listOf(hostName), listOf(ipAddress))
} }
})
logger.info("Approved request $id") logger.info("Approved request $id")
} }
} }
} }
}
val doorman = DoormanServer(HostAndPort.fromParts(host, port), caCertAndKey, rootCACert, storage) val doorman = DoormanServer(HostAndPort.fromParts(host, port), caCertAndKey, rootCACert, storage)
doorman.start() doorman.start()
Runtime.getRuntime().addShutdownHook(thread(start = false) { doorman.close() }) Runtime.getRuntime().addShutdownHook(thread(start = false) { doorman.close() })

View File

@ -26,7 +26,7 @@ interface CertificationRequestStorage {
/** /**
* Approve the given request by generating and storing a new certificate using the provided generator. * Approve the given request by generating and storing a new certificate using the provided generator.
*/ */
fun approveRequest(requestId: String, certificateGenerator: (CertificationRequestData) -> Certificate) fun approveRequest(requestId: String, generateCertificate: CertificationRequestData.() -> Certificate)
/** /**
* Reject the given request using the given reason. * Reject the given request using the given reason.

View File

@ -83,13 +83,13 @@ class DBCertificateRequestStorage(private val database: Database) : Certificatio
} }
} }
override fun approveRequest(requestId: String, certificateGenerator: (CertificationRequestData) -> Certificate) { override fun approveRequest(requestId: String, generateCertificate: CertificationRequestData.() -> Certificate) {
databaseTransaction(database) { databaseTransaction(database) {
val request = singleRequestWhere { DataTable.requestId eq requestId and DataTable.processTimestamp.isNull() } val request = singleRequestWhere { DataTable.requestId eq requestId and DataTable.processTimestamp.isNull() }
if (request != null) { if (request != null) {
withFinalizables { finalizables -> withFinalizables { finalizables ->
DataTable.update({ DataTable.requestId eq requestId }) { DataTable.update({ DataTable.requestId eq requestId }) {
it[certificate] = serializeToBlob(certificateGenerator(request), finalizables) it[certificate] = serializeToBlob(request.generateCertificate(), finalizables)
it[processTimestamp] = Instant.now() it[processTimestamp] = Instant.now()
} }
} }

View File

@ -0,0 +1,65 @@
package com.r3.corda.doorman.persistence
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 net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.commonName
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemObject
import java.io.ByteArrayInputStream
import java.io.StringWriter
import java.security.cert.Certificate
class JiraCertificateRequestStorage(val delegate: CertificationRequestStorage, val jiraClient: JiraRestClient, val projectCode: String, val doneTransitionCode: Int) : CertificationRequestStorage by delegate {
companion object {
val APPROVED = "Approved"
val REJECTED = "Rejected"
}
// The JIRA project must have a Request ID field and the Task issue type.
private val requestIdField = jiraClient.metadataClient.fields.claim().find { it.name == "Request ID" }!!
private val taskIssueType = jiraClient.metadataClient.issueTypes.claim().find { it.name == "Task" }!!
override fun saveRequest(certificationData: CertificationRequestData): String {
val requestId = delegate.saveRequest(certificationData)
// Make sure request has been accepted.
val response = getResponse(requestId)
if (response !is CertificateResponse.Unauthorised) {
val request = StringWriter().use {
JcaPEMWriter(it).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded))
}
it.toString()
}
val commonName = certificationData.request.subject.commonName
val email = certificationData.request.subject.getRDNs(BCStyle.EmailAddress).firstOrNull()?.first?.value
val nearestCity = certificationData.request.subject.getRDNs(BCStyle.L).firstOrNull()?.first?.value
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
.setProjectKey("TD")
.setDescription("Legal Name: $commonName\nNearest City: $nearestCity\nEmail: $email\n\n{code}$request{code}")
.setSummary(commonName)
.setFieldValue(requestIdField.id, requestId)
jiraClient.issueClient.createIssue(issue.build()).fail(Throwable::printStackTrace).claim()
}
return requestId
}
override fun approveRequest(requestId: String, generateCertificate: CertificationRequestData.() -> Certificate) {
delegate.approveRequest(requestId, generateCertificate)
val certificate = (getResponse(requestId) as? CertificateResponse.Ready)?.certificate
val issue = jiraClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
issue?.let {
jiraClient.issueClient.transition(it, TransitionInput(doneTransitionCode)).fail(Throwable::printStackTrace)
jiraClient.issueClient.addAttachment(it.attachmentsUri, ByteArrayInputStream(certificate?.encoded), "${X509Utilities.CORDA_CLIENT_CA}.cer")
}
}
fun getRequestByStatus(status: String): List<String> {
val issues = jiraClient.searchClient.searchJql("project = $projectCode AND status = $status").claim().issues
return issues.map { it.getField(requestIdField.id)?.value?.toString() }.filterNotNull()
}
}

View File

@ -12,3 +12,11 @@ dataSourceProperties {
"dataSource.password" = "" "dataSource.password" = ""
} }
h2port = 0 h2port = 0
jiraConfig{
address = "https://doorman-jira-host/"
projectCode = "TD"
username = "username"
password = "password"
doneTransitionCode = 41
}

View File

@ -86,9 +86,9 @@ class DoormanServiceTest {
assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady) assertThat(pollForResponse(id)).isEqualTo(PollResponse.NotReady)
storage.approveRequest(id) { storage.approveRequest(id) {
JcaPKCS10CertificationRequest(it.request).run { JcaPKCS10CertificationRequest(request).run {
X509Utilities.createServerCert(subject, publicKey, intermediateCA, X509Utilities.createServerCert(subject, publicKey, intermediateCA,
if (it.ipAddress == it.hostName) listOf() else listOf(it.hostName), listOf(it.ipAddress)) if (ipAddress == hostName) listOf() else listOf(hostName), listOf(ipAddress))
} }
} }

View File

@ -133,13 +133,13 @@ class DBCertificateRequestStorageTest {
private fun approveRequest(requestId: String) { private fun approveRequest(requestId: String) {
storage.approveRequest(requestId) { storage.approveRequest(requestId) {
JcaPKCS10CertificationRequest(it.request).run { JcaPKCS10CertificationRequest(request).run {
X509Utilities.createServerCert( X509Utilities.createServerCert(
subject, subject,
publicKey, publicKey,
intermediateCA, intermediateCA,
if (it.ipAddress == it.hostName) listOf() else listOf(it.hostName), if (ipAddress == hostName) listOf() else listOf(hostName),
listOf(it.ipAddress)) listOf(ipAddress))
} }
} }
} }