Merge branch 'master' into demobench

This commit is contained in:
Chris Rankin 2017-02-17 08:23:08 +00:00
commit ac56c7d451
11 changed files with 370 additions and 89 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

@ -0,0 +1,55 @@
package com.r3.corda.doorman
import com.r3.corda.doorman.OptionParserHelper.toConfigWithOptions
import com.typesafe.config.Config
import net.corda.core.div
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.getOrElse
import net.corda.node.services.config.getValue
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
class DoormanParameters(args: Array<String>) {
private val argConfig = args.toConfigWithOptions {
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("rootKeygen", "Generate Root CA keypair and certificate.").withOptionalArg()
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("keystorePassword", "CA keystore password.").withRequiredArg().describedAs("password")
accepts("caPrivateKeyPassword", "CA private key password.").withRequiredArg().describedAs("password")
accepts("rootKeystorePassword", "Root CA keystore password.").withRequiredArg().describedAs("password")
accepts("rootPrivateKeyPassword", "Root private key password.").withRequiredArg().describedAs("password")
accepts("host", "Doorman web service host override").withRequiredArg().describedAs("hostname")
accepts("port", "Doorman web service port override").withRequiredArg().ofType(Int::class.java).describedAs("port number")
}
private val basedir by argConfig.getOrElse { Paths.get(".") }
private val config = argConfig.withFallback(ConfigHelper.loadConfig(basedir, allowMissingConfig = true))
val keystorePath: Path by config.getOrElse { basedir / "certificates" / "caKeystore.jks" }
val rootStorePath: Path by config.getOrElse { basedir / "certificates" / "rootCAKeystore.jks" }
val keystorePassword: String? by config.getOrElse { null }
val caPrivateKeyPassword: String? by config.getOrElse { null }
val rootKeystorePassword: String? by config.getOrElse { null }
val rootPrivateKeyPassword: String? by config.getOrElse { null }
val host: String by config
val port: Int 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 rootKeygen: Boolean by config.getOrElse { false }
val mode = if (rootKeygen) Mode.ROOT_KEYGEN else if (keygen) Mode.CA_KEYGEN else Mode.DOORMAN
enum class Mode {
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,14 +1,27 @@
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.persistence.CertificationRequestStorage
import com.r3.corda.doorman.persistence.DBCertificateRequestStorage import com.r3.corda.doorman.persistence.DBCertificateRequestStorage
import joptsimple.ArgumentAcceptingOptionSpec import com.r3.corda.doorman.persistence.JiraCertificateRequestStorage
import joptsimple.OptionParser import net.corda.core.createDirectories
import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.X509Utilities
import net.corda.core.utilities.debug import net.corda.core.crypto.X509Utilities.CACertAndKey
import net.corda.core.crypto.X509Utilities.CORDA_INTERMEDIATE_CA
import net.corda.core.crypto.X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY
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.addOrReplaceKey
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.loadKeyStore
import net.corda.core.crypto.X509Utilities.loadOrCreateKeyStore
import net.corda.core.crypto.X509Utilities.saveKeyStore
import net.corda.core.seconds
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.services.config.ConfigHelper
import net.corda.node.services.config.getProperties
import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.configureDatabase
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.Server
@ -19,22 +32,30 @@ import org.eclipse.jetty.servlet.ServletHolder
import org.glassfish.jersey.server.ResourceConfig import org.glassfish.jersey.server.ResourceConfig
import org.glassfish.jersey.servlet.ServletContainer import org.glassfish.jersey.servlet.ServletContainer
import java.io.Closeable import java.io.Closeable
import java.lang.Thread.sleep
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.nio.file.Paths import java.net.URI
import java.security.cert.Certificate
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.system.exitProcess import kotlin.system.exitProcess
/** /**
* CertificateSigningServer runs on Jetty server and provide certificate signing service via http. * DoormanServer runs on Jetty server and provide certificate signing service via http.
* The server will require keystorePath, keystore password and key password via command line input. * The server will require keystorePath, keystore password and key password via command line input.
* The Intermediate CA certificate,Intermediate CA private key and Root CA Certificate should use alias name specified in [X509Utilities] * The Intermediate CA certificate,Intermediate CA private key and Root CA Certificate should use alias name specified in [X509Utilities]
*/ */
class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: DoormanWebService) : Closeable {
class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CACertAndKey, val rootCACert: Certificate, val storage: CertificationRequestStorage) : Closeable {
companion object { companion object {
val log = loggerFor<DoormanServer>() val logger = loggerFor<DoormanServer>()
}
private val server: Server = Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply {
handler = HandlerCollection().apply {
addHandler(buildServletContextHandler())
}
} }
private val server: Server = initWebServer()
val hostAndPort: HostAndPort get() = server.connectors val hostAndPort: HostAndPort get() = server.connectors
.map { it as? ServerConnector } .map { it as? ServerConnector }
.filterNotNull() .filterNotNull()
@ -42,20 +63,15 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm
.first() .first()
override fun close() { override fun close() {
log.info("Shutting down CertificateSigningService...") logger.info("Shutting down Doorman Web Services...")
server.stop() server.stop()
server.join() server.join()
} }
private fun initWebServer(): Server { fun start() {
return Server(InetSocketAddress(webServerAddr.hostText, webServerAddr.port)).apply { logger.info("Starting Doorman Web Services...")
log.info("Starting CertificateSigningService...") server.start()
handler = HandlerCollection().apply { logger.info("Doorman Web Services started on $hostAndPort")
addHandler(buildServletContextHandler())
}
start()
log.info("CertificateSigningService started on $hostAndPort")
}
} }
private fun buildServletContextHandler(): ServletContextHandler { private fun buildServletContextHandler(): ServletContextHandler {
@ -63,7 +79,7 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm
contextPath = "/" contextPath = "/"
val resourceConfig = ResourceConfig().apply { val resourceConfig = ResourceConfig().apply {
// Add your API provider classes (annotated for JAX-RS) here // Add your API provider classes (annotated for JAX-RS) here
register(doormanWebService) register(DoormanWebService(caCertAndKey, rootCACert, storage))
} }
val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply { val jerseyServlet = ServletHolder(ServletContainer(resourceConfig)).apply {
initOrder = 0 // Initialise at server start initOrder = 0 // Initialise at server start
@ -73,62 +89,126 @@ class DoormanServer(val webServerAddr: HostAndPort, val doormanWebService: Doorm
} }
} }
object ParamsSpec { /** Read password from console, do a readLine instead if console is null (e.g. when debugging in IDE). */
val parser = OptionParser() private fun readPassword(fmt: String): String {
val basedir: ArgumentAcceptingOptionSpec<String>? = parser.accepts("basedir", "Overriding configuration file path.") return if (System.console() != null) {
.withRequiredArg() String(System.console().readPassword(fmt))
} else {
print(fmt)
readLine()!!
}
}
private fun DoormanParameters.generateRootKeyPair() {
println("Generating Root CA keypair and certificate.")
// Get password from console if not in config.
val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password: ")
// Ensure folder exists.
rootStorePath.parent.createDirectories()
val rootStore = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword)
val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password: ")
if (rootStore.containsAlias(CORDA_ROOT_CA_PRIVATE_KEY)) {
val oldKey = loadOrCreateKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA_PRIVATE_KEY).publicKey
println("Key $CORDA_ROOT_CA_PRIVATE_KEY already exists in keystore, process will now terminate.")
println(oldKey)
exitProcess(1)
}
val selfSignCert = X509Utilities.createSelfSignedCACert(CORDA_ROOT_CA)
rootStore.addOrReplaceKey(CORDA_ROOT_CA_PRIVATE_KEY, selfSignCert.keyPair.private, rootPrivateKeyPassword.toCharArray(), arrayOf(selfSignCert.certificate))
saveKeyStore(rootStore, rootStorePath, rootKeystorePassword)
println("Root CA keypair and certificate stored in $rootStorePath.")
println(loadKeyStore(rootStorePath, rootKeystorePassword).getCertificate(CORDA_ROOT_CA_PRIVATE_KEY).publicKey)
}
private fun DoormanParameters.generateCAKeyPair() {
println("Generating Intermediate CA keypair and certificate using root keystore $rootStorePath.")
// Get password from console if not in config.
val rootKeystorePassword = rootKeystorePassword ?: readPassword("Root Keystore Password: ")
val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password: ")
val rootKeyStore = loadKeyStore(rootStorePath, rootKeystorePassword)
val rootKeyAndCert = loadCertificateAndKey(rootKeyStore, rootPrivateKeyPassword, CORDA_ROOT_CA_PRIVATE_KEY)
val keystorePassword = keystorePassword ?: readPassword("Keystore Password: ")
val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password: ")
// Ensure folder exists.
keystorePath.parent.createDirectories()
val keyStore = loadOrCreateKeyStore(keystorePath, keystorePassword)
if (keyStore.containsAlias(CORDA_INTERMEDIATE_CA_PRIVATE_KEY)) {
val oldKey = loadOrCreateKeyStore(keystorePath, rootKeystorePassword).getCertificate(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).publicKey
println("Key $CORDA_INTERMEDIATE_CA_PRIVATE_KEY already exists in keystore, process will now terminate.")
println(oldKey)
exitProcess(1)
}
val intermediateKeyAndCert = createIntermediateCert(CORDA_INTERMEDIATE_CA, rootKeyAndCert)
keyStore.addOrReplaceKey(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, intermediateKeyAndCert.keyPair.private,
caPrivateKeyPassword.toCharArray(), arrayOf(intermediateKeyAndCert.certificate, rootKeyAndCert.certificate))
saveKeyStore(keyStore, keystorePath, keystorePassword)
println("Intermediate CA keypair and certificate stored in $keystorePath.")
println(loadKeyStore(keystorePath, keystorePassword).getCertificate(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).publicKey)
}
private fun DoormanParameters.startDoorman() {
logger.info("Starting Doorman server.")
// Get password from console if not in config.
val keystorePassword = keystorePassword ?: readPassword("Keystore Password: ")
val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password: ")
val keystore = X509Utilities.loadKeyStore(keystorePath, keystorePassword)
val rootCACert = keystore.getCertificateChain(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).last()
val caCertAndKey = X509Utilities.loadCertificateAndKey(keystore, caPrivateKeyPassword, CORDA_INTERMEDIATE_CA_PRIVATE_KEY)
// Create DB connection.
val (datasource, database) = configureDatabase(dataSourceProperties)
val requestStorage = DBCertificateRequestStorage(database)
val storage = if (jiraConfig == null) {
logger.warn("Doorman server is in 'Approve All' mode, this will approve all incoming certificate signing request.")
// Approve all pending request.
object : CertificationRequestStorage by requestStorage {
// The doorman is in approve all mode, returns all pending request id as approved request id.
override fun getApprovedRequestIds() = getPendingRequestIds()
}
} else {
val jiraClient = AsynchronousJiraRestClientFactory().createWithBasicHttpAuthentication(URI(jiraConfig.address), jiraConfig.username, jiraConfig.password)
JiraCertificateRequestStorage(requestStorage, jiraClient, jiraConfig.projectCode, jiraConfig.doneTransitionCode)
}
// Daemon thread approving request periodically.
thread(name = "Request Approval Daemon") {
while (true) {
sleep(10.seconds.toMillis())
// TODO: Handle rejected request?
for (id in storage.getApprovedRequestIds()) {
storage.approveRequest(id) {
val request = JcaPKCS10CertificationRequest(request)
createServerCert(request.subject, request.publicKey, caCertAndKey,
if (ipAddress == hostName) listOf() else listOf(hostName), listOf(ipAddress))
}
logger.info("Approved request $id")
}
}
}
val doorman = DoormanServer(HostAndPort.fromParts(host, port), caCertAndKey, rootCACert, storage)
doorman.start()
Runtime.getRuntime().addShutdownHook(thread(start = false) { doorman.close() })
} }
fun main(args: Array<String>) { fun main(args: Array<String>) {
val log = DoormanServer.log
log.info("Starting certificate signing server.")
try { try {
ParamsSpec.parser.parse(*args) // TODO : Remove config overrides and solely use config file after testnet is finalized.
} catch (ex: Exception) { DoormanParameters(args).run {
log.error("Unable to parse args", ex) when (mode) {
ParamsSpec.parser.printHelpOn(System.out) DoormanParameters.Mode.ROOT_KEYGEN -> generateRootKeyPair()
exitProcess(1) DoormanParameters.Mode.CA_KEYGEN -> generateCAKeyPair()
}.run { DoormanParameters.Mode.DOORMAN -> startDoorman()
val basedir = Paths.get(valueOf(ParamsSpec.basedir) ?: ".")
val config = ConfigHelper.loadConfig(basedir)
val keystore = X509Utilities.loadKeyStore(Paths.get(config.getString("keystorePath")).normalize(), config.getString("keyStorePassword"))
val intermediateCACertAndKey = X509Utilities.loadCertificateAndKey(keystore, config.getString("caKeyPassword"), X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY)
val rootCA = keystore.getCertificateChain(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY).last()
// Create DB connection.
val (datasource, database) = configureDatabase(config.getProperties("dataSourceProperties"))
val storage = DBCertificateRequestStorage(database)
val service = DoormanWebService(intermediateCACertAndKey, rootCA, storage)
// Background thread approving all request periodically.
var stopSigner = false
val certSinger = if (config.getBoolean("approveAll")) {
thread {
while (!stopSigner) {
Thread.sleep(1000)
for (id in storage.getPendingRequestIds()) {
storage.approveRequest(id, {
JcaPKCS10CertificationRequest(it.request).run {
X509Utilities.createServerCert(subject, publicKey, intermediateCACertAndKey,
if (it.ipAddress == it.hostName) listOf() else listOf(it.hostName), listOf(it.ipAddress))
}
})
log.debug { "Approved $id" }
} }
} }
log.debug { "Certificate Signer thread stopped." } } catch (e: ShowHelpException) {
} e.parser.printHelpOn(System.out)
} else {
null
}
DoormanServer(HostAndPort.fromParts(config.getString("host"), config.getInt("port")), service).use {
Runtime.getRuntime().addShutdownHook(thread(false) {
stopSigner = true
certSinger?.join()
datasource.close()
})
}
} }
} }

View File

@ -0,0 +1,29 @@
package com.r3.corda.doorman
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import joptsimple.ArgumentAcceptingOptionSpec
import joptsimple.OptionParser
/**
* Convert commandline arguments to [Config] object will allow us to use kotlin delegate with [ConfigHelper].
*/
object OptionParserHelper {
fun Array<String>.toConfigWithOptions(registerOptions: OptionParser.() -> Unit): Config {
val parser = OptionParser()
val helpOption = parser.acceptsAll(listOf("h", "?", "help"), "show help").forHelp();
registerOptions(parser)
val optionSet = parser.parse(*this)
// Print help and exit on help option.
if (optionSet.has(helpOption)) {
throw ShowHelpException(parser)
}
// Convert all command line options to Config.
return ConfigFactory.parseMap(parser.recognizedOptions().mapValues {
val optionSpec = it.value
if (optionSpec is ArgumentAcceptingOptionSpec<*> && !optionSpec.requiresArgument() && optionSet.has(optionSpec)) true else optionSpec.value(optionSet)
}.filterValues { it != null })
}
}
class ShowHelpException(val parser: OptionParser) : Exception()

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.
@ -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

@ -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()
} }
} }
@ -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

@ -0,0 +1,76 @@
package com.r3.corda.doorman.persistence
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.TransitionInput
import net.corda.core.crypto.X509Utilities
import net.corda.core.crypto.commonName
import net.corda.core.utilities.loggerFor
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemObject
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 {
private enum class Status {
Approved, Rejected
}
companion object {
private val logger = loggerFor<JiraCertificateRequestStorage>()
}
// The JIRA project must have a Request ID field and the Task issue type.
private val requestIdField: Field = jiraClient.metadataClient.fields.claim().find { it.name == "Request ID" }!!
private val taskIssueType: IssueType = 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()
JcaPEMWriter(request).use {
it.writeObject(PemObject("CERTIFICATE REQUEST", certificationData.request.encoded))
}
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(projectCode)
.setDescription("Legal Name: $commonName\nNearest City: $nearestCity\nEmail: $email\n\n{code}$request{code}")
.setSummary(commonName)
.setFieldValue(requestIdField.id, requestId)
// This will block until the issue is created.
jiraClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim()
}
return requestId
}
override fun approveRequest(requestId: String, generateCertificate: CertificationRequestData.() -> Certificate) {
delegate.approveRequest(requestId, generateCertificate)
// Certificate should be created, retrieving it to attach to the jira task.
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()
if (issue != null) {
jiraClient.issueClient.transition(issue, TransitionInput(doneTransitionCode)).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
jiraClient.issueClient.addAttachment(issue.attachmentsUri, certificate?.encoded?.inputStream(), "${X509Utilities.CORDA_CLIENT_CA}.cer")
.fail { logger.error("Exception when uploading attachment to JIRA.", it) }.claim()
}
}
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
return issues.map { it.getField(requestIdField.id)?.value?.toString() }.filterNotNull()
}
}

View File

@ -1,9 +1,8 @@
host = localhost host = localhost
port = 0 port = 0
keystorePath = ${basedir}"/certificates/keystore.jks" keystorePath = ${basedir}"/certificates/caKeystore.jks"
keyStorePassword = "password" keystorePassword = "password"
caKeyPassword = "password" caPrivateKeyPassword = "password"
approveAll = true
dataSourceProperties { dataSourceProperties {
dataSourceClassName = org.h2.jdbcx.JdbcDataSource dataSourceClassName = org.h2.jdbcx.JdbcDataSource
@ -12,3 +11,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

@ -0,0 +1,20 @@
package com.r3.corda.doorman
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class DoormanParametersTest {
@Test
fun `parse arg correctly`() {
val params = DoormanParameters(arrayOf("--keygen", "--keystorePath", "./testDummyPath.jks"))
assertEquals(DoormanParameters.Mode.CA_KEYGEN, params.mode)
assertEquals("./testDummyPath.jks", params.keystorePath.toString())
assertEquals(0, params.port)
val params2 = DoormanParameters(arrayOf("--keystorePath", "./testDummyPath.jks", "--port", "1000"))
assertEquals(DoormanParameters.Mode.DOORMAN, params2.mode)
assertEquals("./testDummyPath.jks", params2.keystorePath.toString())
assertEquals(1000, params2.port)
}
}

View File

@ -31,7 +31,8 @@ class DoormanServiceTest {
private lateinit var doormanServer: DoormanServer private lateinit var doormanServer: DoormanServer
private fun startSigningServer(storage: CertificationRequestStorage) { private fun startSigningServer(storage: CertificationRequestStorage) {
doormanServer = DoormanServer(HostAndPort.fromParts("localhost", 0), DoormanWebService(intermediateCA, rootCA.certificate, storage)) doormanServer = DoormanServer(HostAndPort.fromParts("localhost", 0), intermediateCA, rootCA.certificate, storage)
doormanServer.start()
} }
@After @After
@ -85,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))
} }
} }
} }