Addressed PR issues

This commit is contained in:
Patrick Kuo 2017-02-16 11:30:03 +00:00
parent 234863f88c
commit d76db6d047
7 changed files with 44 additions and 31 deletions

View File

@ -15,7 +15,6 @@ class DoormanParameters(args: Array<String>) {
accepts("basedir", "Overriding configuration filepath, default to current directory.").withRequiredArg().describedAs("filepath") accepts("basedir", "Overriding configuration filepath, default to current directory.").withRequiredArg().describedAs("filepath")
accepts("keygen", "Generate CA keypair and certificate using provide Root CA key.").withOptionalArg() accepts("keygen", "Generate CA keypair and certificate using provide Root CA key.").withOptionalArg()
accepts("rootKeygen", "Generate Root CA keypair and certificate.").withOptionalArg() accepts("rootKeygen", "Generate Root CA keypair and certificate.").withOptionalArg()
accepts("approveAll", "Approve all certificate signing request.").withOptionalArg()
accepts("keystorePath", "CA keystore filepath, default to [basedir]/certificates/caKeystore.jks.").withRequiredArg().describedAs("filepath") accepts("keystorePath", "CA keystore filepath, default to [basedir]/certificates/caKeystore.jks.").withRequiredArg().describedAs("filepath")
accepts("rootStorePath", "Root CA keystore filepath, default to [basedir]/certificates/rootCAKeystore.jks.").withRequiredArg().describedAs("filepath") accepts("rootStorePath", "Root CA keystore filepath, default to [basedir]/certificates/rootCAKeystore.jks.").withRequiredArg().describedAs("filepath")
accepts("keystorePassword", "CA keystore password.").withRequiredArg().describedAs("password") accepts("keystorePassword", "CA keystore password.").withRequiredArg().describedAs("password")
@ -33,7 +32,6 @@ class DoormanParameters(args: Array<String>) {
val caPrivateKeyPassword: String? by config.getOrElse { null } val caPrivateKeyPassword: String? by config.getOrElse { null }
val rootKeystorePassword: String? by config.getOrElse { null } val rootKeystorePassword: String? by config.getOrElse { null }
val rootPrivateKeyPassword: String? by config.getOrElse { null } val rootPrivateKeyPassword: String? by config.getOrElse { null }
val approveAll: Boolean by config.getOrElse { false }
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

View File

@ -164,12 +164,14 @@ private fun DoormanParameters.startDoorman() {
// Create DB connection. // Create DB connection.
val (datasource, database) = configureDatabase(dataSourceProperties) val (datasource, database) = configureDatabase(dataSourceProperties)
val storage = if (approveAll) { val storage = if (jiraConfig == null) {
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) // Approve all pending request.
object : CertificationRequestStorage by DBCertificateRequestStorage(database) {
override fun getApprovedRequestIds() = getPendingRequestIds()
}
} else { } else {
// Require JIRA config to be non-null. val jiraClient = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
val jiraClient = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig!!.address), jiraConfig.username, jiraConfig.password)
JiraCertificateRequestStorage(DBCertificateRequestStorage(database), jiraClient, jiraConfig.projectCode, jiraConfig.doneTransitionCode) JiraCertificateRequestStorage(DBCertificateRequestStorage(database), jiraClient, jiraConfig.projectCode, jiraConfig.doneTransitionCode)
} }
@ -177,8 +179,8 @@ private fun DoormanParameters.startDoorman() {
thread(name = "Request Approval Daemon") { thread(name = "Request Approval Daemon") {
while (true) { while (true) {
sleep(10.seconds.toMillis()) sleep(10.seconds.toMillis())
val approvedRequests = (storage as? JiraCertificateRequestStorage)?.getRequestByStatus(JiraCertificateRequestStorage.APPROVED) ?: storage.getPendingRequestIds() // TODO: Handle rejected request?
for (id in approvedRequests) { for (id in storage.getApprovedRequestIds()) {
storage.approveRequest(id) { storage.approveRequest(id) {
val request = JcaPKCS10CertificationRequest(request) val request = JcaPKCS10CertificationRequest(request)
createServerCert(request.subject, request.publicKey, caCertAndKey, createServerCert(request.subject, request.publicKey, caCertAndKey,

View File

@ -35,9 +35,13 @@ interface CertificationRequestStorage {
/** /**
* Retrieve list of request IDs waiting for approval. * Retrieve list of request IDs waiting for approval.
* TODO : This is used for the background thread to approve request automatically without KYC checks, should be removed after testnet.
*/ */
fun getPendingRequestIds(): List<String> fun getPendingRequestIds(): List<String>
/**
* Retrieve list of approved request IDs.
*/
fun getApprovedRequestIds(): List<String>
} }
data class CertificationRequestData(val hostName: String, val ipAddress: String, val request: PKCS10CertificationRequest) data class CertificationRequestData(val hostName: String, val ipAddress: String, val request: PKCS10CertificationRequest)

View File

@ -121,6 +121,8 @@ class DBCertificateRequestStorage(private val database: Database) : Certificatio
} }
} }
override fun getApprovedRequestIds(): List<String> = emptyList()
private fun singleRequestWhere(where: SqlExpressionBuilder.() -> Op<Boolean>): CertificationRequestData? { private fun singleRequestWhere(where: SqlExpressionBuilder.() -> Op<Boolean>): CertificationRequestData? {
return DataTable return DataTable
.select(where) .select(where)

View File

@ -1,64 +1,73 @@
package com.r3.corda.doorman.persistence package com.r3.corda.doorman.persistence
import com.atlassian.jira.rest.client.api.JiraRestClient import com.atlassian.jira.rest.client.api.JiraRestClient
import com.atlassian.jira.rest.client.api.domain.Field
import com.atlassian.jira.rest.client.api.domain.IssueType
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.commonName import net.corda.core.crypto.commonName
import net.corda.core.utilities.loggerFor
import org.bouncycastle.asn1.x500.style.BCStyle import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemObject import org.bouncycastle.util.io.pem.PemObject
import java.io.ByteArrayInputStream
import java.io.StringWriter import java.io.StringWriter
import java.security.cert.Certificate import java.security.cert.Certificate
class JiraCertificateRequestStorage(val delegate: CertificationRequestStorage, val jiraClient: JiraRestClient, val projectCode: String, val doneTransitionCode: Int) : CertificationRequestStorage by delegate { class JiraCertificateRequestStorage(val delegate: CertificationRequestStorage,
companion object { val jiraClient: JiraRestClient,
val APPROVED = "Approved" val projectCode: String,
val REJECTED = "Rejected" val doneTransitionCode: Int) : CertificationRequestStorage by delegate {
private enum class Status {
Approved, Rejected
} }
private val logger = loggerFor<JiraCertificateRequestStorage>()
// The JIRA project must have a Request ID field and the Task issue type. // 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 requestIdField: Field = jiraClient.metadataClient.fields.claim().find { it.name == "Request ID" }!!
private val taskIssueType = jiraClient.metadataClient.issueTypes.claim().find { it.name == "Task" }!! private val taskIssueType: IssueType = jiraClient.metadataClient.issueTypes.claim().find { it.name == "Task" }!!
override fun saveRequest(certificationData: CertificationRequestData): String { override fun saveRequest(certificationData: CertificationRequestData): String {
val requestId = delegate.saveRequest(certificationData) val requestId = delegate.saveRequest(certificationData)
// Make sure request has been accepted. // Make sure request has been accepted.
val response = getResponse(requestId) val response = getResponse(requestId)
if (response !is CertificateResponse.Unauthorised) { if (response !is CertificateResponse.Unauthorised) {
val request = StringWriter().use { val request = StringWriter()
JcaPEMWriter(it).use { JcaPEMWriter(request).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded)) it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded))
}
it.toString()
} }
val commonName = certificationData.request.subject.commonName val commonName = certificationData.request.subject.commonName
val email = certificationData.request.subject.getRDNs(BCStyle.EmailAddress).firstOrNull()?.first?.value val email = certificationData.request.subject.getRDNs(BCStyle.EmailAddress).firstOrNull()?.first?.value
val nearestCity = certificationData.request.subject.getRDNs(BCStyle.L).firstOrNull()?.first?.value val nearestCity = certificationData.request.subject.getRDNs(BCStyle.L).firstOrNull()?.first?.value
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id) val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
.setProjectKey("TD") .setProjectKey(projectCode)
.setDescription("Legal Name: $commonName\nNearest City: $nearestCity\nEmail: $email\n\n{code}$request{code}") .setDescription("Legal Name: $commonName\nNearest City: $nearestCity\nEmail: $email\n\n{code}$request{code}")
.setSummary(commonName) .setSummary(commonName)
.setFieldValue(requestIdField.id, requestId) .setFieldValue(requestIdField.id, requestId)
// This will block until the jira is created.
jiraClient.issueClient.createIssue(issue.build()).fail(Throwable::printStackTrace).claim() jiraClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
} }
return requestId return requestId
} }
override fun approveRequest(requestId: String, generateCertificate: CertificationRequestData.() -> Certificate) { override fun approveRequest(requestId: String, generateCertificate: CertificationRequestData.() -> Certificate) {
delegate.approveRequest(requestId, generateCertificate) delegate.approveRequest(requestId, generateCertificate)
// Certificate should be created, retrieving it to attach to the jira task.
val certificate = (getResponse(requestId) as? CertificateResponse.Ready)?.certificate val certificate = (getResponse(requestId) as? CertificateResponse.Ready)?.certificate
// Jira only support ~ (contains) search for custom textfield.
val issue = jiraClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull() val issue = jiraClient.searchClient.searchJql("'Request ID' ~ $requestId").claim().issues.firstOrNull()
issue?.let { if (issue != null) {
jiraClient.issueClient.transition(it, TransitionInput(doneTransitionCode)).fail(Throwable::printStackTrace) jiraClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
jiraClient.issueClient.addAttachment(it.attachmentsUri, ByteArrayInputStream(certificate?.encoded), "${X509Utilities.CORDA_CLIENT_CA}.cer") jiraClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
.fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim()
} }
} }
fun getRequestByStatus(status: String): List<String> { override fun getApprovedRequestIds(): List<String> = getRequestByStatus(Status.Approved)
private fun getRequestByStatus(status: Status): List<String> {
val issues = jiraClient.searchClient.searchJql("project = $projectCode AND status = $status").claim().issues val issues = jiraClient.searchClient.searchJql("project = $projectCode AND status = $status").claim().issues
return issues.map { it.getField(requestIdField.id)?.value?.toString() }.filterNotNull() return issues.map { it.getField(requestIdField.id)?.value?.toString() }.filterNotNull()
} }

View File

@ -3,7 +3,6 @@ port = 0
keystorePath = ${basedir}"/certificates/caKeystore.jks" keystorePath = ${basedir}"/certificates/caKeystore.jks"
keystorePassword = "password" keystorePassword = "password"
caPrivateKeyPassword = "password" caPrivateKeyPassword = "password"
approveAll = true
dataSourceProperties { dataSourceProperties {
dataSourceClassName = org.h2.jdbcx.JdbcDataSource dataSourceClassName = org.h2.jdbcx.JdbcDataSource

View File

@ -7,11 +7,10 @@ import kotlin.test.assertTrue
class DoormanParametersTest { class DoormanParametersTest {
@Test @Test
fun `parse arg correctly`() { fun `parse arg correctly`() {
val params = DoormanParameters(arrayOf("--keygen", "--keystorePath", "./testDummyPath.jks", "--approveAll")) val params = DoormanParameters(arrayOf("--keygen", "--keystorePath", "./testDummyPath.jks"))
assertEquals(DoormanParameters.Mode.CA_KEYGEN, params.mode) assertEquals(DoormanParameters.Mode.CA_KEYGEN, params.mode)
assertEquals("./testDummyPath.jks", params.keystorePath.toString()) assertEquals("./testDummyPath.jks", params.keystorePath.toString())
assertEquals(0, params.port) assertEquals(0, params.port)
assertTrue(params.approveAll)
val params2 = DoormanParameters(arrayOf("--keystorePath", "./testDummyPath.jks", "--port", "1000")) val params2 = DoormanParameters(arrayOf("--keystorePath", "./testDummyPath.jks", "--port", "1000"))
assertEquals(DoormanParameters.Mode.DOORMAN, params2.mode) assertEquals(DoormanParameters.Mode.DOORMAN, params2.mode)