mirror of
https://github.com/corda/corda.git
synced 2024-12-29 09:18:58 +00:00
Doorman-Jira integration
This commit is contained in:
parent
7cd8d952a9
commit
234863f88c
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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() })
|
||||||
|
@ -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.
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user