diff --git a/adapter/adapter-service/src/main/scala/net/shrine/adapter/audit/AdapterAuditDb.scala b/adapter/adapter-service/src/main/scala/net/shrine/adapter/audit/AdapterAuditDb.scala index 2e2a67f4b..3b8da27bd 100644 --- a/adapter/adapter-service/src/main/scala/net/shrine/adapter/audit/AdapterAuditDb.scala +++ b/adapter/adapter-service/src/main/scala/net/shrine/adapter/audit/AdapterAuditDb.scala @@ -1,320 +1,321 @@ package net.shrine.adapter.audit import java.sql.SQLException import javax.sql.DataSource import com.typesafe.config.Config import net.shrine.adapter.service.AdapterConfigSource import net.shrine.audit.{NetworkQueryId, QueryName, QueryTopicId, QueryTopicName, ShrineNodeId, Time, UserName} import net.shrine.crypto.KeyStoreCertCollection +import net.shrine.crypto2.KeyStoreEntry import net.shrine.log.Loggable import net.shrine.protocol.{BroadcastMessage, RunQueryRequest, RunQueryResponse, ShrineResponse} import net.shrine.slick.TestableDataSourceCreator import slick.driver.JdbcProfile import scala.concurrent.duration.{Duration, DurationInt} import scala.concurrent.{Await, Future, blocking} import scala.language.postfixOps /** * DB code for the Adapter audit metrics. * * @author david * @since 8/25/15 */ case class AdapterAuditDb(schemaDef:AdapterAuditSchema,dataSource: DataSource) extends Loggable { import schemaDef._ import jdbcProfile.api._ val database = Database.forDataSource(dataSource) def createTables() = schemaDef.createTables(database) def dropTables() = schemaDef.dropTables(database) def dbRun[R](action: DBIOAction[R, NoStream, Nothing]):R = { val future: Future[R] = database.run(action) blocking { Await.result(future, 10 seconds) } } def insertQueryReceived(broadcastMessage: BroadcastMessage):Unit = { debug(s"insertQueryReceived $broadcastMessage") QueryReceived.fromBroadcastMessage(broadcastMessage).foreach(insertQueryReceived) } def insertQueryReceived(queryReceived:QueryReceived):Unit = { dbRun(allQueriesReceived += queryReceived) } def selectAllQueriesReceived:Seq[QueryReceived] = { dbRun(allQueriesReceived.result) } def insertExecutionStarted(runQueryRequest: RunQueryRequest):Unit = { debug(s"insertExecutionStarted $runQueryRequest") insertExecutionStarted(ExecutionStarted.fromRequest(runQueryRequest)) } def insertExecutionStarted(executionStart:ExecutionStarted):Unit = { dbRun(allExecutionsStarted += executionStart) } def selectAllExecutionStarts:Seq[ExecutionStarted] = { dbRun(allExecutionsStarted.result) } def insertExecutionCompletedShrineResponse(request: RunQueryRequest,shrineResponse: ShrineResponse) = { debug(s"insertExecutionCompleted $shrineResponse for $request") ExecutionCompleted.fromRequestResponse(request,shrineResponse).foreach(insertExecutionCompleted) } def insertExecutionCompleted(executionCompleted:ExecutionCompleted):Unit = { dbRun(allExecutionsCompleted += executionCompleted) } def selectAllExecutionCompletes:Seq[ExecutionCompleted] = { dbRun(allExecutionsCompleted.result) } def insertResultSent(networkQueryId: NetworkQueryId,shrineResponse:ShrineResponse):Unit = { debug(s"insertResultSent $shrineResponse for $networkQueryId") ResultSent.fromResponse(networkQueryId,shrineResponse).foreach(insertResultSent) } def insertResultSent(resultSent: ResultSent):Unit = { dbRun(allResultsSent += resultSent) } def selectAllResultsSent:Seq[ResultSent] = { dbRun(allResultsSent.result) } } /** * Separate class to support schema generation without actually connecting to the database. * * @param jdbcProfile Database profile to use for the schema */ case class AdapterAuditSchema(jdbcProfile: JdbcProfile) extends Loggable { import jdbcProfile.api._ def ddlForAllTables = { allQueriesReceived.schema ++ allExecutionsStarted.schema ++ allExecutionsCompleted.schema ++ allResultsSent.schema } //to get the schema, use the REPL //println(AdapterAuditSchema.schema.ddlForAllTables.createStatements.mkString(";\n")) def createTables(database:Database) = { try { val future = database.run(ddlForAllTables.create) Await.result(future,10 seconds) } catch { //I'd prefer to check and create schema only if absent. No way to do that with Oracle. case x:SQLException => info("Caught exception while creating tables. Recover by assuming the tables already exist.",x) } } def dropTables(database:Database) = { val future = database.run(ddlForAllTables.drop) //Really wait forever for the cleanup Await.result(future,Duration.Inf) } class QueriesReceivedAuditTable(tag:Tag) extends Table[QueryReceived](tag,"queriesReceived") { def shrineNodeId = column[ShrineNodeId]("shrineNodeId") def userName = column[UserName]("userName") def networkQueryId = column[NetworkQueryId]("networkQueryId") def queryName = column[QueryName]("queryName") def queryTopicId = column[Option[QueryTopicId]]("topicId") def queryTopicName = column[Option[QueryTopicName]]("topicName") def timeQuerySent = column[Time]("timeQuerySent") def timeQueryReceived = column[Time]("timeReceived") def * = (shrineNodeId,userName,networkQueryId,queryName,queryTopicId,queryTopicName,timeQuerySent,timeQueryReceived) <> (QueryReceived.tupled,QueryReceived.unapply) } val allQueriesReceived = TableQuery[QueriesReceivedAuditTable] class ExecutionsStartedTable(tag:Tag) extends Table[ExecutionStarted](tag,"executionsStarted") { def networkQueryId = column[NetworkQueryId]("networkQueryId") def queryName = column[QueryName]("queryName") def timeExecutionStarts = column[Time]("timeExecutionStarted") def * = (networkQueryId,queryName,timeExecutionStarts) <> (ExecutionStarted.tupled,ExecutionStarted.unapply) } val allExecutionsStarted = TableQuery[ExecutionsStartedTable] class ExecutionsCompletedTable(tag:Tag) extends Table[ExecutionCompleted](tag,"executionsCompleted") { def networkQueryId = column[NetworkQueryId]("networkQueryId") def replyId = column[Long]("replyId") def queryName = column[QueryName]("queryName") def timeExecutionCompletes = column[Time]("timeExecutionCompleted") def * = (networkQueryId,replyId,queryName,timeExecutionCompletes) <> (ExecutionCompleted.tupled,ExecutionCompleted.unapply) } val allExecutionsCompleted = TableQuery[ExecutionsCompletedTable] class ResultsSentTable(tag:Tag) extends Table[ResultSent](tag,"resultsSent") { def networkQueryId = column[NetworkQueryId]("networkQueryId") def replyId = column[Long]("replyId") def queryName = column[QueryName]("queryName") def timeResultsSent = column[Time]("timeResultsSent") def * = (networkQueryId,replyId,queryName,timeResultsSent) <> (ResultSent.tupled,ResultSent.unapply) } val allResultsSent = TableQuery[ResultsSentTable] } object AdapterAuditSchema { val allConfig:Config = AdapterConfigSource.config val config:Config = allConfig.getConfig("shrine.adapter.audit.database") val slickProfile:JdbcProfile = AdapterConfigSource.getObject("slickProfileClassName", config) val schema = AdapterAuditSchema(slickProfile) } object AdapterAuditDb { val dataSource:DataSource = TestableDataSourceCreator.dataSource(AdapterAuditSchema.config) val db = AdapterAuditDb(AdapterAuditSchema.schema,dataSource) val createTablesOnStart = AdapterAuditSchema.config.getBoolean("createTablesOnStart") if(createTablesOnStart) AdapterAuditDb.db.createTables() } case class QueryReceived( shrineNodeId:ShrineNodeId, userName:UserName, networkQueryId:NetworkQueryId, queryName:QueryName, queryTopicId:Option[QueryTopicId], queryTopicName:Option[QueryTopicName], timeQuerySent:Time, timeQueryReceived:Time ) object QueryReceived extends (( ShrineNodeId, UserName, NetworkQueryId, QueryName, Option[QueryTopicId], Option[QueryTopicName], Time, Time ) => QueryReceived) with Loggable { def fromBroadcastMessage(message:BroadcastMessage):Option[QueryReceived] = { message.request match { case rqr:RunQueryRequest => val timestampAndShrineNodeCn:(Time,ShrineNodeId) = message.signature.fold{ warn(s"No signature on message ${message.requestId}") (-1L,"No Cert For Message")}{signature => val timesamp = signature.timestamp.toGregorianCalendar.getTimeInMillis - val shrineNodeId:ShrineNodeId = signature.signingCert.fold("Signing Cert Not Available")(x => KeyStoreCertCollection.extractCommonName(x.toCertificate).getOrElse("Common name not in cert")) + val shrineNodeId:ShrineNodeId = KeyStoreEntry.extractCommonName(signature.value.toArray).getOrElse("Signing Cert Not Available") (timesamp,shrineNodeId) } Some(QueryReceived(timestampAndShrineNodeCn._2, message.networkAuthn.username, rqr.networkQueryId, rqr.queryDefinition.name, rqr.topicId, rqr.topicName, timestampAndShrineNodeCn._1, System.currentTimeMillis() )) case _ => None } } } case class ExecutionStarted( networkQueryId:NetworkQueryId, queryName:QueryName, timeExecutionStarted:Time ) object ExecutionStarted extends (( NetworkQueryId, QueryName, Time ) => ExecutionStarted){ def fromRequest(rqr:RunQueryRequest) = { ExecutionStarted(rqr.networkQueryId, rqr.queryDefinition.name, System.currentTimeMillis()) } } case class ExecutionCompleted( networkQueryId:NetworkQueryId, replyId:Long, queryName:QueryName, timeExecutionCompleted:Time ) object ExecutionCompleted extends (( NetworkQueryId, Long, QueryName, Time ) => ExecutionCompleted){ def fromRequestResponse(request: RunQueryRequest,shrineResponse:ShrineResponse) = { shrineResponse match { case rqr:RunQueryResponse => Some(ExecutionCompleted( request.networkQueryId, rqr.queryId, rqr.queryName, System.currentTimeMillis())) case _ => None } } } case class ResultSent( networkQueryId:NetworkQueryId, responseId:Long, queryName:QueryName, timeResultSent:Time ) object ResultSent extends (( NetworkQueryId, Long, QueryName, Time ) => ResultSent){ def fromResponse(networkQueryId:NetworkQueryId,shrineResponse:ShrineResponse) = { shrineResponse match { case rqr:RunQueryResponse => Some(ResultSent( networkQueryId, rqr.queryId, rqr.queryName, System.currentTimeMillis())) case _ => None } } } diff --git a/adapter/adapter-service/src/main/scala/net/shrine/adapter/service/AdapterService.scala b/adapter/adapter-service/src/main/scala/net/shrine/adapter/service/AdapterService.scala index 635db13e5..060bdad4b 100644 --- a/adapter/adapter-service/src/main/scala/net/shrine/adapter/service/AdapterService.scala +++ b/adapter/adapter-service/src/main/scala/net/shrine/adapter/service/AdapterService.scala @@ -1,102 +1,101 @@ package net.shrine.adapter.service import net.shrine.log.Loggable import net.shrine.protocol.{BaseShrineResponse, BroadcastMessage, ErrorResponse, NodeId, RequestType, Result, Signature} import net.shrine.adapter.AdapterMap import net.shrine.crypto.Verifier import net.shrine.problem.{AbstractProblem, ProblemSources} import scala.concurrent.duration.Duration import scala.concurrent.duration._ /** * Heart of the adapter. * * @author clint * @since Nov 14, 2013 */ final class AdapterService( nodeId: NodeId, signatureVerifier: Verifier, maxSignatureAge: Duration, adapterMap: AdapterMap) extends AdapterRequestHandler with Loggable { import AdapterService._ logStartup(adapterMap) override def handleRequest(message: BroadcastMessage): Result = { handleInvalidSignature(message).orElse { for { adapter <- adapterMap.adapterFor(message.request.requestType) } yield time(nodeId) { adapter.perform(message) } }.getOrElse { Result(nodeId, 0.milliseconds, ErrorResponse(UnknownRequestType(message.request.requestType))) } } /** * @return None if the signature is fine, Some(result with an ErrorResponse) if not */ private def handleInvalidSignature(message: BroadcastMessage): Option[Result] = { val (sigIsValid, elapsed) = time(signatureVerifier.verifySig(message, maxSignatureAge)) - println(s"HEY! $sigIsValid") if(sigIsValid) { None } else { info(s"Incoming message had invalid signature: $message") Some(Result(nodeId, elapsed.milliseconds, ErrorResponse(CouldNotVerifySignature(message)))) } } } object AdapterService extends Loggable { private def logStartup(adapterMap: AdapterMap) { info("Adapter service initialized, will respond to the following queries: ") val sortedByReqType = adapterMap.requestsToAdapters.toSeq.sortBy { case (k, _) => k } sortedByReqType.foreach { case (requestType, adapter) => info(s" $requestType:\t(${adapter.getClass.getSimpleName})") } } private[service] def time[T](f: => T): (T, Long) = { val start = System.currentTimeMillis val result = f val elapsed = System.currentTimeMillis - start (result, elapsed) } private[service] def time(nodeId: NodeId)(f: => BaseShrineResponse): Result = { val (response, elapsed) = time(f) Result(nodeId, elapsed.milliseconds, response) } } case class CouldNotVerifySignature(message: BroadcastMessage) extends AbstractProblem(ProblemSources.Adapter){ val signature: Option[Signature] = message.signature override val summary: String = signature.fold("A message was not signed")(sig => s"The trust relationship with ${sig.signedBy} is not properly configured.") override val description: String = signature.fold(s"The Adapter at ${stamp.host.getHostName} could not properly validate a request because it had no signature.")(sig => s"The Adapter at ${stamp.host.getHostName} could not properly validate the request from ${sig.signedBy}. An incoming message from the hub had an invalid signature.") override val detailsXml = signature.fold(
)( sig =>
Signature is {sig}
) } case class UnknownRequestType(requestType: RequestType) extends AbstractProblem(ProblemSources.Adapter){ override val summary: String = s"Unknown request type $requestType" override val description: String = s"The Adapter at ${stamp.host.getHostName} received a request of type $requestType that it cannot process." } \ No newline at end of file diff --git a/apps/dashboard-app/src/main/scala/net/shrine/dashboard/DashboardService.scala b/apps/dashboard-app/src/main/scala/net/shrine/dashboard/DashboardService.scala index e61e8b50e..d7004b695 100644 --- a/apps/dashboard-app/src/main/scala/net/shrine/dashboard/DashboardService.scala +++ b/apps/dashboard-app/src/main/scala/net/shrine/dashboard/DashboardService.scala @@ -1,521 +1,522 @@ package net.shrine.dashboard import akka.actor.Actor import akka.event.Logging import net.shrine.authentication.UserAuthenticator import net.shrine.authorization.steward.OutboundUser import net.shrine.config.ConfigExtensions import net.shrine.crypto.{KeyStoreCertCollection, KeyStoreDescriptorParser, UtilHasher} +import net.shrine.crypto2.{BouncyKeyStoreCollection, CertCollectionAdapter} import net.shrine.dashboard.jwtauth.ShrineJwtAuthenticator import net.shrine.i2b2.protocol.pm.User import net.shrine.status.protocol.{Config => StatusProtocolConfig} import net.shrine.dashboard.httpclient.HttpClientDirectives.{forwardUnmatchedPath, requestUriThenRoute} import net.shrine.log.Loggable import net.shrine.problem.{ProblemDigest, Problems} import net.shrine.serialization.NodeSeqSerializer import shapeless.HNil import spray.http.{HttpRequest, HttpResponse, StatusCodes, Uri} import spray.httpx.Json4sSupport import spray.routing.directives.LogEntry import spray.routing._ import org.json4s.{DefaultFormats, Formats} import org.json4s.native.JsonMethods.{parse => json4sParse} import scala.collection.immutable.Iterable import scala.concurrent.ExecutionContext.Implicits.global /** * Mixes the DashboardService trait with an Akka Actor to provide the actual service. */ class DashboardServiceActor extends Actor with DashboardService { // the HttpService trait defines only one abstract member, which // connects the services environment to the enclosing actor or test def actorRefFactory = context // this actor only runs our route, but you could add // other things here, like request stream processing // or timeout handling def receive = runRoute(route) } /** * A web service that provides the Dashboard endpoints. It is a trait to support testing independent of Akka. */ trait DashboardService extends HttpService with Loggable { val userAuthenticator = UserAuthenticator(DashboardConfigSource.config) //don't need to do anything special for unauthorized users, but they do need access to a static form. lazy val route:Route = gruntWatchCorsSupport { redirectToIndex ~ staticResources ~ makeTrouble ~ about ~ authenticatedInBrowser ~ authenticatedDashboard } /** logs the request method, uri and response at info level */ def logEntryForRequestResponse(req: HttpRequest): Any => Option[LogEntry] = { case res: HttpResponse => Some(LogEntry(s"\n Request: $req\n Response: $res", Logging.InfoLevel)) case _ => None // other kind of responses } /** logs just the request method, uri and response status at info level */ def logEntryForRequest(req: HttpRequest): Any => Option[LogEntry] = { case res: HttpResponse => Some(LogEntry(s"\n Request: $req\n Response status: ${res.status}", Logging.InfoLevel)) case _ => None // other kind of responses } def authenticatedInBrowser: Route = pathPrefixTest("user"|"admin"|"toDashboard") { logRequestResponse(logEntryForRequestResponse _) { //logging is controlled by Akka's config, slf4j, and log4j config reportIfFailedToAuthenticate { authenticate(userAuthenticator.basicUserAuthenticator) { user => pathPrefix("user") { userRoute(user) } ~ pathPrefix("admin") { adminRoute(user) } ~ pathPrefix("toDashboard") { toDashboardRoute(user) } } } } } val reportIfFailedToAuthenticate = routeRouteResponse { case Rejected(List(AuthenticationFailedRejection(_,_))) => complete("AuthenticationFailed") } def authenticatedDashboard:Route = pathPrefix("fromDashboard") { logRequestResponse(logEntryForRequestResponse _) { //logging is controlled by Akka's config, slf4j, and log4j config get { //all remote dashboard calls are gets. authenticate(ShrineJwtAuthenticator.authenticate) { user => adminRoute(user) } } } } def makeTrouble = pathPrefix("makeTrouble") { complete(throw new IllegalStateException("fake trouble")) } lazy val redirectToIndex = pathEnd { redirect("shrine-dashboard/client/index.html", StatusCodes.PermanentRedirect) //todo pick up "shrine-dashboard" programatically } ~ ( path("index.html") | pathSingleSlash) { redirect("client/index.html", StatusCodes.PermanentRedirect) } lazy val staticResources = pathPrefix("client") { pathEnd { redirect("client/index.html", StatusCodes.PermanentRedirect) } ~ pathSingleSlash { redirect("index.html", StatusCodes.PermanentRedirect) } ~ { getFromResourceDirectory("client") } } lazy val about = pathPrefix("about") { complete("Nothing here yet") //todo } def userRoute(user:User):Route = get { pathPrefix("whoami") { complete(OutboundUser.createFromUser(user)) } } //todo check that this an admin. def adminRoute(user:User):Route = get { pathPrefix("happy") { val happyBaseUrl: String = DashboardConfigSource.config.getString("shrine.dashboard.happyBaseUrl") forwardUnmatchedPath(happyBaseUrl) } ~ pathPrefix("messWithHappyVersion") { //todo is this used? val happyBaseUrl: String = DashboardConfigSource.config.getString("shrine.dashboard.happyBaseUrl") def pullClasspathFromConfig(httpResponse:HttpResponse,uri:Uri):Route = { ctx => { val result = httpResponse.entity.asString ctx.complete(s"Got '$result' from $uri") } } requestUriThenRoute(happyBaseUrl+"/version",pullClasspathFromConfig) } ~ pathPrefix("ping") { complete("pong") }~ pathPrefix("status") { statusRoute(user) } } ~ post { pathPrefix("status") pathPrefix("verifySignature") verifySignature } //Manually test this by running a curl command //curl -k -w "\n%{response_code}\n" -u dave:kablam "https://shrine-dev1.catalyst:6443/shrine-dashboard/toDashboard/shrine-dev2.catalyst/ping" /** * Forward a request from this dashboard to a remote dashboard */ def toDashboardRoute(user:User):Route = get { pathPrefix(Segment) { dnsName => val remoteDashboardProtocol = DashboardConfigSource.config.getString("shrine.dashboard.remoteDashboard.protocol") val remoteDashboardPort = DashboardConfigSource.config.getString("shrine.dashboard.remoteDashboard.port") val remoteDashboardPathPrefix = DashboardConfigSource.config.getString("shrine.dashboard.remoteDashboard.pathPrefix") val baseUrl = s"$remoteDashboardProtocol$dnsName$remoteDashboardPort/$remoteDashboardPathPrefix" forwardUnmatchedPath(baseUrl,Some(ShrineJwtAuthenticator.createOAuthCredentials(user))) } } def statusRoute(user:User):Route = get { val( adapter , hub , i2b2 , keystore , optionalParts , qep , summary ) = ("adapter", "hub", "i2b2", "keystore", "optionalParts", "qep", "summary") pathPrefix("classpath") { getClasspath }~ pathPrefix("config") { getConfig }~ pathPrefix("problems") { getProblems }~ pathPrefix(adapter) { getFromSubService(adapter) }~ pathPrefix(hub) { getFromSubService(hub) }~ pathPrefix(i2b2) { getFromSubService(i2b2) }~ pathPrefix(keystore) { getFromSubService(keystore) }~ pathPrefix(optionalParts) { getFromSubService(optionalParts) }~ pathPrefix(qep) { getFromSubService(qep) }~ pathPrefix(summary) { getFromSubService(summary) } } val statusBaseUrl = DashboardConfigSource.config.getString("shrine.dashboard.statusBaseUrl") // TODO: check all other certs and return a list of domain/had sig pairs. // lazy val checkCertCollection:Route = { // // } // TODO: Move this over to Status API? lazy val verifySignature:Route = { val keyStoreDescriptor = DashboardConfigSource.config.getConfigured("shrine.keystore", KeyStoreDescriptorParser(_)) - val certCollection: KeyStoreCertCollection = KeyStoreCertCollection.fromFileRecoverWithClassPath(keyStoreDescriptor) - val hasher = UtilHasher(certCollection) + val certCollection = BouncyKeyStoreCollection.fromFileRecoverWithClassPath(keyStoreDescriptor) + val hasher = UtilHasher(CertCollectionAdapter(certCollection)) def handleSig(sha256:String): Option[Boolean] = { if (hasher.validSignatureFormat(sha256)) { hasher.containsCertWithSig(sha256).map(_ => true) } else { Some(false) } } // Intellij complains if you use formFields with multiple params ¯\_(ツ)_/¯ formField("sha256".as[String].?) { sha256: Option[String] => val badRequest: StandardRoute = complete(StatusCodes.BadRequest, "You must provide a valid SHA-256 signature to verify along with its alias") val notFound: StandardRoute = complete(StatusCodes.NotFound, "Could not find a certificate matching the given signature") val foundCert: StandardRoute = complete(StatusCodes.OK, "A matching cert with a matching SHA-256 signature was found") sha256.fold(badRequest)(sha => handleSig(sha) .fold(notFound) (if (_) foundCert else badRequest)) } } lazy val getConfig:Route = { def completeConfigRoute(httpResponse:HttpResponse,uri:Uri):Route = { ctx => { val config = ParsedConfig(httpResponse.entity.asString) ctx.complete( ShrineConfig(config) ) } } requestUriThenRoute(statusBaseUrl + "/config", completeConfigRoute) } lazy val getClasspath:Route = { def pullClasspathFromConfig(httpResponse:HttpResponse,uri:Uri):Route = { ctx => { val result = httpResponse.entity.asString val shrineConfig = ShrineConfig(ParsedConfig(result)) ctx.complete(shrineConfig) } } requestUriThenRoute(statusBaseUrl + "/config",pullClasspathFromConfig) } def getFromSubService(key: String):Route = { requestUriThenRoute(s"$statusBaseUrl/$key") } // table based view, can see N problems at a time. Front end sends how many problems that they want // to skip, and it will take N the 'nearest N' ie with n = 20, 0-19 -> 20, 20-39 -> 20-40 lazy val getProblems:Route = { def floorMod(x: Int, y: Int) = { x - (x % y) } val db = Problems.DatabaseConnector // Intellij loudly complains if you use parameters instead of chained parameter calls. // ¯\_(ツ)_/¯ parameter("offset".as[Int].?(0)) {(offsetPreMod:Int) => { parameter("n".as[Int].?(20)) {(nPreMax:Int) => parameter("epoch".as[Long].?) {(epoch:Option[Long]) => val n = Math.max(0, nPreMax) val moddedOffset = floorMod(Math.max(0, offsetPreMod), n) val query = for { result <- db.IO.sizeAndProblemDigest(n, moddedOffset) } yield (result._2, floorMod(Math.max(0, moddedOffset), n), n, result._1) val query2 = for { dateOffset <- db.IO.findIndexOfDate(epoch.getOrElse(0)) moddedOffset = floorMod(dateOffset, n) result <- db.IO.sizeAndProblemDigest(n, moddedOffset) } yield (result._2, moddedOffset, n, result._1) val queryReal = if (epoch.isEmpty) query else query2 val tupled = db.runBlocking(queryReal) val response: ProblemResponse = ProblemResponse(tupled._1, tupled._2, tupled._3, tupled._4) implicit val formats = response.json4sMarshaller complete(response) }}}} } } case class ProblemResponse(size: Int, offset: Int, n: Int, problems: Seq[ProblemDigest]) extends Json4sSupport { override implicit def json4sFormats: Formats = DefaultFormats + new NodeSeqSerializer } /** * Centralized parsing logic for map of shrine.conf * the class literal `T.class` in Java. */ //todo most of this info should come directly from the status service in Shrine, not from reading the config case class ParsedConfig(configMap:Map[String, String]){ private val trueVal = "true" private val rootKey = "shrine" def isHub = getOrElse(rootKey + ".hub.create", "") .toLowerCase == trueVal def stewardEnabled = configMap.keySet .contains(rootKey + ".queryEntryPoint.shrineSteward") def shouldQuerySelf = getOrElse(rootKey + ".hub.shouldQuerySelf", "") .toLowerCase == trueVal def fromJsonString(jsonString:String): String = jsonString.split("\"").mkString("") def get(key:String): Option[String] = configMap.get(key).map(fromJsonString) def getOrElse(key:String, elseVal:String = ""): String = get(key).getOrElse(elseVal) } object ParsedConfig { def apply(jsonString:String):ParsedConfig = { implicit def json4sFormats: Formats = DefaultFormats ParsedConfig(json4sParse(jsonString).extract[StatusProtocolConfig].keyValues)//.filterKeys(_.toLowerCase.startsWith("shrine"))) } } case class DownstreamNode(name:String, url:String) object DownstreamNode { def create(configMap:Map[String,String]):Iterable[DownstreamNode] = { for ((k, v) <- configMap.filterKeys(_.toLowerCase.startsWith ("shrine.hub.downstreamnodes"))) yield DownstreamNode(k.split('.').last,v.split("\"").mkString("")) } } //todo replace with the actual config, scrubbed of passwords case class ShrineConfig(isHub:Boolean, hub:Hub, pmEndpoint:Endpoint, ontEndpoint:Endpoint, hiveCredentials: HiveCredentials, adapter: Adapter, queryEntryPoint:QEP, networkStatusQuery:String, configMap:Map[String, String] ) extends DefaultJsonSupport object ShrineConfig extends DefaultJsonSupport { def apply(config:ParsedConfig):ShrineConfig = { val hub = Hub(config) val isHub = config.isHub val pmEndpoint = Endpoint("pm",config) val ontEndpoint = Endpoint("ont",config) val hiveCredentials = HiveCredentials(config) val adapter = Adapter(config) val queryEntryPoint = QEP(config) val networkStatusQuery = config.configMap("shrine.networkStatusQuery") ShrineConfig(isHub, hub, pmEndpoint, ontEndpoint, hiveCredentials, adapter, queryEntryPoint, networkStatusQuery, config.configMap) } } case class Endpoint(acceptAllCerts:Boolean, url:String, timeoutSeconds:Int) object Endpoint{ def apply(endpointType:String,parsedConfig:ParsedConfig):Endpoint = { val prefix = "shrine." + endpointType.toLowerCase + "Endpoint." val acceptAllCerts = parsedConfig.configMap.getOrElse(prefix + "acceptAllCerts", "") == "true" val url = parsedConfig.configMap.getOrElse(prefix + "url","") val timeoutSeconds = parsedConfig.configMap.getOrElse(prefix + "timeout.seconds", "0").toInt Endpoint(acceptAllCerts, url, timeoutSeconds) } } case class HiveCredentials(domain:String, username:String, password:String, crcProjectId:String, ontProjectId:String) object HiveCredentials{ def apply(parsedConfig:ParsedConfig):HiveCredentials = { val key = "shrine.hiveCredentials." val domain = parsedConfig.configMap.getOrElse(key + "domain","") val username = parsedConfig.configMap.getOrElse(key + "username","") val password = "REDACTED" val crcProjectId = parsedConfig.configMap.getOrElse(key + "crcProjectId","") val ontProjectId = parsedConfig.configMap.getOrElse(key + "ontProjectId","") HiveCredentials(domain, username, password, crcProjectId, ontProjectId) } } // -- hub only -- // //todo delete when the Dashboard front end can use the status service's hub method case class Hub(shouldQuerySelf:Boolean, create:Boolean, downstreamNodes:Iterable[DownstreamNode]) object Hub{ def apply(parsedConfig:ParsedConfig):Hub = { val shouldQuerySelf = parsedConfig.shouldQuerySelf val create = parsedConfig.isHub val downstreamNodes = DownstreamNode.create(parsedConfig.configMap) Hub(shouldQuerySelf, create, downstreamNodes) } } // -- adapter info -- // case class Adapter(crcEndpointUrl:String, setSizeObfuscation:Boolean, adapterLockoutAttemptsThreshold:Int, adapterMappingsFilename:String) object Adapter{ def apply(parsedConfig:ParsedConfig):Adapter = { val key = "shrine.adapter." val crcEndpointUrl = parsedConfig.configMap.getOrElse(key + "crcEndpoint.url","") val setSizeObfuscation = parsedConfig.configMap.getOrElse(key + "setSizeObfuscation","").toLowerCase == "true" val adapterLockoutAttemptsThreshold = parsedConfig.configMap.getOrElse(key + "adapterLockoutAttemptsThreshold", "0").toInt val adapterMappingsFileName = parsedConfig.configMap.getOrElse(key + "adapterMappingsFileName","") Adapter(crcEndpointUrl, setSizeObfuscation, adapterLockoutAttemptsThreshold, adapterMappingsFileName) } } case class Steward(qepUserName:String, stewardBaseUrl:String) object Steward { def apply (parsedConfig:ParsedConfig):Steward = { val key = "shrine.queryEntryPoint.shrineSteward." val qepUserName = parsedConfig.configMap.getOrElse(key + "qepUserName","") val stewardBaseUrl = parsedConfig.configMap.getOrElse(key + "stewardBaseUrl","") Steward(qepUserName, stewardBaseUrl) } } // -- if needed -- // case class TimeoutInfo (timeUnit:String, description:String) case class DatabaseInfo(createTablesOnStart:Boolean, dataSourceFrom:String, jndiDataSourceName:String, slickProfileClassName:String) case class Audit(database:DatabaseInfo, collectQepAudit:Boolean) object Audit{ def apply(parsedConfig:ParsedConfig):Audit = { val key = "shrine.queryEntryPoint.audit." val createTablesOnStart = parsedConfig.configMap.getOrElse(key + "database.createTablesOnStart","") == "true" val dataSourceFrom = parsedConfig.configMap.getOrElse(key + "database.dataSourceFrom","") val jndiDataSourceName = parsedConfig.configMap.getOrElse(key + "database.jndiDataSourceName","") val slickProfileClassName = parsedConfig.configMap.getOrElse(key + "database.slickProfileClassName","") val collectQepAudit = parsedConfig.configMap.getOrElse(key + "collectQepAudit","") == "true" val database = DatabaseInfo(createTablesOnStart, dataSourceFrom, jndiDataSourceName, slickProfileClassName) Audit(database, collectQepAudit) } } case class QEP( maxQueryWaitTimeMinutes:Int, create:Boolean, attachSigningCert:Boolean, authorizationType:String, includeAggregateResults:Boolean, authenticationType:String, audit:Audit, shrineSteward:Steward, broadcasterServiceEndpointUrl:Option[String] ) object QEP{ val key = "shrine.queryEntryPoint." def apply(parsedConfig:ParsedConfig):QEP = QEP( maxQueryWaitTimeMinutes = parsedConfig.configMap.getOrElse(key + "maxQueryWaitTime.minutes", "0").toInt, create = parsedConfig.configMap.getOrElse(key + "create","") == "true", attachSigningCert = parsedConfig.configMap.getOrElse(key + "attachSigningCert","") == "true", authorizationType = parsedConfig.configMap.getOrElse(key + "authorizationType",""), includeAggregateResults = parsedConfig.configMap.getOrElse(key + "includeAggregateResults","") == "true", authenticationType = parsedConfig.configMap.getOrElse(key + "authenticationType", ""), audit = Audit(parsedConfig), shrineSteward = Steward(parsedConfig), broadcasterServiceEndpointUrl = parsedConfig.configMap.get(key + "broadcasterServiceEndpoint.url") ) } //adapted from https://gist.github.com/joseraya/176821d856b43b1cfe19 object gruntWatchCorsSupport extends Directive0 with RouteConcatenation { import spray.http.HttpHeaders.{`Access-Control-Allow-Methods`, `Access-Control-Max-Age`, `Access-Control-Allow-Headers`,`Access-Control-Allow-Origin`} import spray.routing.directives.RespondWithDirectives.respondWithHeaders import spray.routing.directives.MethodDirectives.options import spray.routing.directives.RouteDirectives.complete import spray.http.HttpMethods.{OPTIONS,GET,POST} import spray.http.AllOrigins private val allowOriginHeader = `Access-Control-Allow-Origin`(AllOrigins) private val optionsCorsHeaders = List( `Access-Control-Allow-Headers`("Origin, X-Requested-With, Content-Type, Accept, Accept-Encoding, Accept-Language, Host, Referer, User-Agent, Authorization"), `Access-Control-Max-Age`(1728000)) //20 days val gruntWatch:Boolean = DashboardConfigSource.config.getBoolean("shrine.dashboard.gruntWatch") override def happly(f: (HNil) => Route): Route = { if(gruntWatch) { options { respondWithHeaders(`Access-Control-Allow-Methods`(OPTIONS, GET, POST) :: allowOriginHeader :: optionsCorsHeaders){ complete(StatusCodes.OK) } } ~ f(HNil) } else f(HNil) } } trait DefaultJsonSupport extends Json4sSupport { override implicit def json4sFormats: Formats = DefaultFormats } \ No newline at end of file diff --git a/apps/dashboard-app/src/main/scala/net/shrine/dashboard/jwtauth/ShrineJwtAuthenticator.scala b/apps/dashboard-app/src/main/scala/net/shrine/dashboard/jwtauth/ShrineJwtAuthenticator.scala index 3dcb996b1..2d5cd7140 100644 --- a/apps/dashboard-app/src/main/scala/net/shrine/dashboard/jwtauth/ShrineJwtAuthenticator.scala +++ b/apps/dashboard-app/src/main/scala/net/shrine/dashboard/jwtauth/ShrineJwtAuthenticator.scala @@ -1,202 +1,212 @@ package net.shrine.dashboard.jwtauth import java.io.ByteArrayInputStream -import java.security.{Principal, Key, PrivateKey} +import java.security.{Key, Principal, PrivateKey} import java.security.cert.{CertificateFactory, X509Certificate} import java.util.Date import io.jsonwebtoken.impl.TextCodec -import io.jsonwebtoken.{Jws, ClaimJwtException, Claims, SignatureAlgorithm, Jwts} -import net.shrine.crypto.{KeyStoreDescriptorParser, KeyStoreCertCollection} +import io.jsonwebtoken.{ClaimJwtException, Claims, Jws, Jwts, SignatureAlgorithm} +import net.shrine.crypto.{KeyStoreCertCollection, KeyStoreDescriptorParser} +import net.shrine.crypto2.{BouncyKeyStoreCollection, HubCertCollection, PeerCertCollection} import net.shrine.dashboard.DashboardConfigSource import net.shrine.i2b2.protocol.pm.User import net.shrine.log.Loggable import net.shrine.protocol.Credential import spray.http.HttpHeaders.{Authorization, `WWW-Authenticate`} -import spray.http.{HttpRequest, OAuth2BearerToken, HttpHeader, HttpChallenge} +import spray.http.{HttpChallenge, HttpHeader, HttpRequest, OAuth2BearerToken} import spray.routing.AuthenticationFailedRejection.{CredentialsMissing, CredentialsRejected} import spray.routing.AuthenticationFailedRejection import spray.routing.authentication._ -import scala.concurrent.{Future, ExecutionContext} -import scala.util.{Success, Failure, Try} +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success, Try} /** * An Authenticator that uses Jwt in a Bearer header to authenticate. See http://jwt.io/introduction/ for what this is all about, * https://tools.ietf.org/html/rfc7519 for what it might include for claims. * * @author david * @since 12/21/15 */ object ShrineJwtAuthenticator extends Loggable { val config = DashboardConfigSource.config - val certCollection: KeyStoreCertCollection = KeyStoreCertCollection.fromFileRecoverWithClassPath(KeyStoreDescriptorParser(config.getConfig("shrine.keystore"))) + val certCollection = BouncyKeyStoreCollection.fromFileRecoverWithClassPath(KeyStoreDescriptorParser(config.getConfig("shrine.keystore"))) //from https://groups.google.com/forum/#!topic/spray-user/5DBEZUXbjtw def authenticate(implicit ec: ExecutionContext): ContextAuthenticator[User] = { ctx => Future { val attempt: Try[Authentication[User]] = for { header:HttpHeader <- extractAuthorizationHeader(ctx.request) jwtsString:String <- extractJwtsStringAndCheckScheme(header) jwtsClaims <- extractJwtsClaims(jwtsString) cert: X509Certificate <- extractAndCheckCert(jwtsClaims) jwtsBody:Claims <- Try{jwtsClaims.getBody} jwtsSubject <- failIfNull(jwtsBody.getSubject,MissingRequiredJwtsClaim("subject",cert.getSubjectDN)) jwtsIssuer <- failIfNull(jwtsBody.getSubject,MissingRequiredJwtsClaim("issuer",cert.getSubjectDN)) } yield { val user = User( fullName = cert.getSubjectDN.getName, username = jwtsSubject, domain = jwtsIssuer, credential = Credential(jwtsIssuer, isToken = false), params = Map(), rolesByProject = Map() ) Right(user) } //todo use a fold() in Scala 2.12 attempt match { case Success(rightUser) => rightUser case Failure(x) => x match { case anticipated: ShrineJwtException => info(s"Failed to authenticate due to ${anticipated.toString}",anticipated) anticipated.rejection case fromJwts: ClaimJwtException => info(s"Failed to authenticate due to ${fromJwts.toString} while authenticating ${ctx.request}",fromJwts) rejectedCredentials /* case x: CertificateExpiredException => { //todo will these even be thrown here? Get some identification here info(s"Cert expired.", x) rejectedCredentials } case x: CertificateNotYetValidException => { info(s"Cert not yet valid.", x) rejectedCredentials } */ case unanticipated => warn(s"Unanticipated ${unanticipated.toString} while authenticating ${ctx.request}",unanticipated) rejectedCredentials } } } } def createOAuthCredentials(user:User): OAuth2BearerToken = { - val base64Cert:String = TextCodec.BASE64URL.encode(certCollection.myCert.get.getEncoded) + val base64Cert:String = TextCodec.BASE64URL.encode(certCollection.myEntry.cert.getEncoded) - val key: PrivateKey = certCollection.myKeyPair.privateKey + val key: PrivateKey = certCollection.myEntry.privateKey.get val expiration: Date = new Date(System.currentTimeMillis() + 30 * 1000) //good for 30 seconds val jwtsString = Jwts.builder(). setHeaderParam("kid", base64Cert). setSubject(s"${user.username} at ${user.domain}"). setIssuer(java.net.InetAddress.getLocalHost.getHostName). //todo is it OK for me to use issuer this way or should I use my own claim? setExpiration(expiration). signWith(SignatureAlgorithm.RS512, key). compact() OAuth2BearerToken(jwtsString) } def extractAuthorizationHeader(request: HttpRequest):Try[HttpHeader] = Try { case class NoAuthorizationHeaderException(request: HttpRequest) extends ShrineJwtException(s"No ${Authorization.name} header found in $request",missingCredentials) //noinspection ComparingUnrelatedTypes request.headers.find(_.name.equals(Authorization.name)).getOrElse{throw NoAuthorizationHeaderException(request)} } def extractJwtsStringAndCheckScheme(httpHeader: HttpHeader) = Try { val splitHeaderValue: Array[String] = httpHeader.value.trim.split(" ") if (splitHeaderValue.length != 2) { case class WrongNumberOfSegmentsException(httpHeader: HttpHeader) extends ShrineJwtException(s"Header had ${splitHeaderValue.length} space-delimited segments, not 2, in $httpHeader.",missingCredentials) throw new WrongNumberOfSegmentsException(httpHeader) } else if(splitHeaderValue(0) != BearerAuthScheme) { case class NotBearerAuthException(httpHeader: HttpHeader) extends ShrineJwtException(s"Expected $BearerAuthScheme, not ${splitHeaderValue(0)} in $httpHeader.",missingCredentials) throw new NotBearerAuthException(httpHeader) } else splitHeaderValue(1) } def extractJwtsClaims(jwtsString:String): Try[Jws[Claims]] = Try { Jwts.parser().setSigningKeyResolver(new SigningKeyResolverBridge()).parseClaimsJws(jwtsString) } def extractAndCheckCert(jwtsClaims:Jws[Claims]): Try[X509Certificate] = Try { val cert = KeySource.certForString(jwtsClaims.getHeader.getKeyId) val issuingSite = jwtsClaims.getBody.getIssuer - //todo is this the right way to find a cert in the certCollection? + case class CertIssuerNotInCollectionException(issuingSite:String,issuer: Principal, aliases: Iterable[String]) extends ShrineJwtException(s"Could not find a certificate with issuer DN $issuer. Known cert aliases are ${aliases.mkString(",")}") - debug(s"certCollection.caCerts.contains(${cert.getSubjectX500Principal}) is ${certCollection.caCerts.contains(cert.getSubjectX500Principal)}") - certCollection.caCerts.get(cert.getSubjectX500Principal).fold{ - //if not in the keystore, check that the issuer is available - val issuer: Principal = cert.getIssuerX500Principal - case class CertIssuerNotInCollectionException(issuingSite:String,issuer: Principal) extends ShrineJwtException(s"Could not find a CA certificate with issuer DN $issuer. Known CA cert aliases are ${certCollection.caCertAliases.mkString(",")}") - val signingCert = certCollection.caCerts.getOrElse(issuer,{throw CertIssuerNotInCollectionException(issuingSite,issuer)}) - - //verify that the cert was signed using the signingCert - //todo this can throw CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException - cert.verify(signingCert.getPublicKey) - //todo has cert expired? - info(s"${cert.getSubjectX500Principal} verified using $issuer from the KeyStore") - cert - }{ principal => //if the cert is in the certCollection then all is well - info(s"$principal is in the KeyStore") - cert + //todo is this the right way to find a cert in the certCollection? + certCollection match { + case HubCertCollection(_, caEntry) if caEntry.signed(cert) => caEntry.cert + case px:PeerCertCollection => px.allEntries.find(_.signed(cert)).map(_.cert).getOrElse( + throw CertIssuerNotInCollectionException(issuingSite,cert.getIssuerDN, px.allEntries.flatMap(_.aliases)) + ) + case hc:HubCertCollection => throw CertIssuerNotInCollectionException(issuingSite,cert.getIssuerDN, hc.caEntry.aliases) } + +// debug(s"certCollection.caCerts.contains(${cert.getSubjectX500Principal}) is ${caEntry.cert.getSubjectX500Principal == cert.getSubjectX500Principal}") +// certCollection.caCerts.get(cert.getSubjectX500Principal).fold{ +// //if not in the keystore, check that the issuer is available +// val issuer: Principal = cert.getIssuerX500Principal +// case class CertIssuerNotInCollectionException(issuingSite:String,issuer: Principal) extends ShrineJwtException(s"Could not find a CA certificate with issuer DN $issuer. Known CA cert aliases are ${certCollection.caCertAliases.mkString(",")}") +// val signingCert = certCollection.caCerts.getOrElse(issuer,{throw CertIssuerNotInCollectionException(issuingSite,issuer)}) +// +// //verify that the cert was signed using the signingCert +// //todo this can throw CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException +// cert.verify(signingCert.getPublicKey) +// //todo has cert expired? +// info(s"${cert.getSubjectX500Principal} verified using $issuer from the KeyStore") +// cert +// }{ principal => //if the cert is in the certCollection then all is well +// info(s"$principal is in the KeyStore") +// cert +// } } def failIfNull[E](e:E,t:Throwable):Try[E] = Try { if(e == null) throw t else e } case class MissingRequiredJwtsClaim(field:String,principal: Principal) extends ShrineJwtException(s"$field is null from ${principal.getName}") val BearerAuthScheme = "Bearer" val challengeHeader: `WWW-Authenticate` = `WWW-Authenticate`(HttpChallenge(BearerAuthScheme, "dashboard-to-dashboard")) val missingCredentials: Authentication[User] = Left(AuthenticationFailedRejection(CredentialsMissing, List(challengeHeader))) val rejectedCredentials: Authentication[User] = Left(AuthenticationFailedRejection(CredentialsRejected, List(challengeHeader))) } class KeySource {} object KeySource extends Loggable { def keyForString(string: String): Key = { val certificate =certForString(string) //todo validate cert with something like obtainAndValidateSigningCert //check date on cert vs time. throws CertificateExpiredException or CertificateNotYetValidException for problems //todo skip this until you rebuild the certs used for testing certificate.checkValidity(now) certificate.getPublicKey } def certForString(string: String): X509Certificate = { val certBytes = TextCodec.BASE64URL.decode(string) val inputStream = new ByteArrayInputStream(certBytes) val certificate = try { CertificateFactory.getInstance("X.509").generateCertificate(inputStream).asInstanceOf[X509Certificate] } finally { inputStream.close() } certificate } } abstract class ShrineJwtException(message:String, val rejection:Authentication[User] = ShrineJwtAuthenticator.rejectedCredentials, cause:Throwable = null) extends RuntimeException(message,cause) \ No newline at end of file diff --git a/apps/dashboard-app/src/test/resources/shrine.conf b/apps/dashboard-app/src/test/resources/shrine.conf index a3edd6875..dca269c35 100644 --- a/apps/dashboard-app/src/test/resources/shrine.conf +++ b/apps/dashboard-app/src/test/resources/shrine.conf @@ -1,54 +1,55 @@ shrine { problem { problemHandler = "net.shrine.problem.DatabaseProblemHandler$" } authenticate { usersource { //Bogus security for testing type = "ConfigUserSource" //Must be ConfigUserSource (for isolated testing) or PmUserSource (for everything else) researcher { username = "ben" password = "kapow" } steward { username = "dave" password = "kablam" } qep{ username = "qep" password = "trustme" } admin{ username = "keith" password = "shh!" } } } dashboard { happyBaseUrl = "classpath://resources/testhappy" statusBaseUrl = "classpath://resources/teststatus" database { dataSourceFrom = "testDataSource" slickProfileClassName = "slick.driver.H2Driver$" createTestValuesOnStart = true createTablesOnStart = true // For testing without JNDI testDataSource { //typical test settings for unit tests driverClassName = "org.h2.Driver" url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" } } } keystore { file = "shrine.keystore" - password = "chiptesting" - privateKeyAlias = "test-cert" + password = "justatestpassword" + privateKeyAlias = "shrine-test" keyStoreType = "JKS" - caCertAliases = [carra ca] + caCertAliases = ["test-cert-ca"] + trustModelIsHub = true } } \ No newline at end of file diff --git a/apps/dashboard-app/src/test/resources/shrine.keystore b/apps/dashboard-app/src/test/resources/shrine.keystore index faa5158ea..918effcbc 100644 Binary files a/apps/dashboard-app/src/test/resources/shrine.keystore and b/apps/dashboard-app/src/test/resources/shrine.keystore differ diff --git a/apps/dashboard-app/src/test/scala/net/shrine/dashboard/DashboardServiceTest.scala b/apps/dashboard-app/src/test/scala/net/shrine/dashboard/DashboardServiceTest.scala index 4d1a88e2f..e62536cf4 100644 --- a/apps/dashboard-app/src/test/scala/net/shrine/dashboard/DashboardServiceTest.scala +++ b/apps/dashboard-app/src/test/scala/net/shrine/dashboard/DashboardServiceTest.scala @@ -1,415 +1,415 @@ package net.shrine.dashboard import java.security.PrivateKey import java.util.Date import io.jsonwebtoken.impl.TextCodec import io.jsonwebtoken.{Jwts, SignatureAlgorithm} import net.shrine.authorization.steward.OutboundUser import net.shrine.crypto.{KeyStoreCertCollection, KeyStoreDescriptorParser} import net.shrine.dashboard.jwtauth.ShrineJwtAuthenticator import net.shrine.i2b2.protocol.pm.User import net.shrine.protocol.Credential import org.json4s.native.JsonMethods.parse import org.junit.runner.RunWith import org.scalatest.FlatSpec import org.scalatest.junit.JUnitRunner import spray.http.StatusCodes.{NotFound, OK, PermanentRedirect, Unauthorized} import spray.http.{BasicHttpCredentials, FormData, OAuth2BearerToken, StatusCodes} import spray.testkit.ScalatestRouteTest import scala.language.postfixOps @RunWith(classOf[JUnitRunner]) class DashboardServiceTest extends FlatSpec with ScalatestRouteTest with DashboardService { def actorRefFactory = system import scala.concurrent.duration._ implicit val routeTestTimeout = RouteTestTimeout(10 seconds) val adminUserName = "keith" val adminFullName = adminUserName /** * to run these tests with I2B2 * add a user named keith, to be the admin * add a Boolean parameter for keith, Admin, true * add all this user to the i2b2 project */ val adminCredentials = BasicHttpCredentials(adminUserName,"shh!") val brokenCredentials = BasicHttpCredentials(adminUserName,"wrong password") val adminUser = User( fullName = adminUserName, username = adminFullName, domain = "domain", credential = new Credential("admin's password",false), params = Map(), rolesByProject = Map() ) val adminOutboundUser = OutboundUser.createFromUser(adminUser) "DashboardService" should "return an OK and a valid outbound user for a user/whoami request" in { Get(s"/user/whoami") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) implicit val formats = OutboundUser.json4sFormats val userJson = new String(body.data.toByteArray) val outboundUser = parse(userJson).extract[OutboundUser] assertResult(adminOutboundUser)(outboundUser) } } "DashboardService" should "return an OK and a valid outbound user for a user/whoami request and an '' " in { Get(s"/user/whoami") ~> addCredentials(brokenCredentials) ~> route ~> check { assertResult(OK)(status) val response = new String(body.data.toByteArray) assertResult("""AuthenticationFailed""")(response) } } "DashboardService" should "redirect several urls to client/index.html" in { Get() ~> route ~> check { status === PermanentRedirect header("Location") === "client/index.html" } Get("/") ~> route ~> check { status === PermanentRedirect header("Location") === "client/index.html" } Get("/index.html") ~> route ~> check { status === PermanentRedirect header("Location") === "client/index.html" } Get("/client") ~> route ~> check { status === PermanentRedirect header("Location") === "client/index.html" } Get("/client/") ~> route ~> check { status === PermanentRedirect header("Location") === "client/index.html" } } "DashboardService" should "return an OK and the right version string for an admin/happy/all test" in { Get(s"/admin/happy/all") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val allString = new String(body.data.toByteArray) //todo test it to see if it's right } } "DashboardService" should "return an OK and mess with the right version string for an admin/messWithHappyVersion test" in { Get(s"/admin/messWithHappyVersion") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val versionString = new String(body.data.toByteArray) //todo test it to see if it's right } } "DashboardService" should "return an OK for admin/status/config" in { Get(s"/admin/status/config") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val configString = new String(body.data.toByteArray) } } "DashboardService" should "return an OK for admin/status/classpath" in { Get(s"/admin/status/classpath") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val classpathString = new String(body.data.toByteArray) } } "DashboardService" should "return an OK for admin/status/optionalParts" in { Get(s"/admin/status/optionalParts") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val options = new String(body.data.toByteArray) } } "DashboardService" should "return an OK for admin/status/summary" in { Get(s"/admin/status/summary") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val summary = new String(body.data.toByteArray) } } "DashboardService" should "return an OK for admin/status/adapter" in { Get(s"/admin/status/adapter") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val adapter = new String(body.data.toByteArray) } } "DashboardService" should "return an OK for admin/status/hub" in { Get(s"/admin/status/hub") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val hub = new String(body.data.toByteArray) } } "DashboardService" should "return an OK for admin/status/i2b2" in { Get(s"/admin/status/i2b2") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val i2b2 = new String(body.data.toByteArray) } } "DashboardService" should "return an OK for admin/status/keystore" in { Get(s"/admin/status/keystore") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val keystore = new String(body.data.toByteArray) } } "DashboardService" should "return an OK for admin/status/qep" in { Get(s"/admin/status/qep") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val qep = new String(body.data.toByteArray) } } "DashboardService" should "return an OK for admin/status/problems" in { Get("/admin/status/problems") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val problems = new String(body.data.toByteArray) } } "DashboardService" should "return an OK for admin/status/problems with queries" in { Get("/admin/status/problems?offset=2&n=1") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val problems = new String(body.data.toByteArray) } } "DashboardService" should "return an OK for admin/status/problems with queries and an epoch filter" in { Get("/admin/status/problems?offset=2&n=3&epoch=3") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) val problems = new String(body.data.toByteArray) } } "DashboardService" should "return a BadRequest for admin/status/signature with no signature parameter" in { Post("/admin/status/verifySignature") ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(StatusCodes.BadRequest)(status) } } "DashboardService" should "return a BadRequest for admin/status/signature with a malformatted signature parameter" in { Post("/admin/status/verifySignature", FormData(Seq("sha256" -> "foo"))) ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(StatusCodes.BadRequest)(status) } } "DashboardService" should "return a NotFound for admin/status/signature with a correctly formatted parameter that is not in the keystore" in { Post("/admin/status/verifySignature", FormData(Seq("sha256" -> "00:00:00:00:00:00:00:7C:4B:FD:8D:A8:0A:C7:A4:AA:13:3E:22:B3:57:A7:C6:B0:95:15:1B:22:C0:E5:15:9A"))) ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(StatusCodes.NotFound)(status) } } "DashboardService" should "return an OK for admin/status/signature with a valid sha256 hash" in { - Post("/admin/status/verifySignature", FormData(Seq("sha256" -> "AB:56:98:69:F0:DD:3C:7C:4B:FD:8D:A8:0A:C7:A4:AA:13:3E:22:B3:57:A7:C6:B0:95:15:1B:22:C0:E5:15:9A"))) ~> + Post("/admin/status/verifySignature", FormData(Seq("sha256" -> "65:AA:60:6C:CD:56:1F:C2:A6:90:AE:C9:01:61:96:B2:A5:EA:A5:05:A5:55:27:18:24:45:73:8F:15:A9:09:03"))) ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(StatusCodes.OK)(status) } } val dashboardCredentials = BasicHttpCredentials(adminUserName,"shh!") "DashboardService" should "return an OK and pong for fromDashboard/ping" in { Get(s"/fromDashboard/ping") ~> addCredentials(ShrineJwtAuthenticator.createOAuthCredentials(adminUser)) ~> route ~> check { assertResult(OK)(status) val string = new String(body.data.toByteArray) assertResult("pong")(string) } } "DashboardService" should "reject a fromDashboard/ping with an expired jwts header" in { val config = DashboardConfigSource.config val shrineCertCollection: KeyStoreCertCollection = KeyStoreCertCollection.fromFileRecoverWithClassPath(KeyStoreDescriptorParser(config.getConfig("shrine.keystore"))) val base64Cert = new String(TextCodec.BASE64URL.encode(shrineCertCollection.myCert.get.getEncoded)) val key: PrivateKey = shrineCertCollection.myKeyPair.privateKey val expiration: Date = new Date(System.currentTimeMillis() - 300 * 1000) //bad for 5 minutes val jwtsString = Jwts.builder(). setHeaderParam("kid", base64Cert). setSubject(java.net.InetAddress.getLocalHost.getHostName). setExpiration(expiration). signWith(SignatureAlgorithm.RS512, key). compact() Get(s"/fromDashboard/ping") ~> addCredentials(OAuth2BearerToken(jwtsString)) ~> sealRoute(route) ~> check { assertResult(Unauthorized)(status) } } "DashboardService" should "reject a fromDashboard/ping with no subject" in { val config = DashboardConfigSource.config val shrineCertCollection: KeyStoreCertCollection = KeyStoreCertCollection.fromClassPathResource(KeyStoreDescriptorParser(config.getConfig("shrine.keystore"))) val base64Cert = new String(TextCodec.BASE64URL.encode(shrineCertCollection.myCert.get.getEncoded)) val key: PrivateKey = shrineCertCollection.myKeyPair.privateKey val expiration: Date = new Date(System.currentTimeMillis() + 30 * 1000) val jwtsString = Jwts.builder(). setHeaderParam("kid", base64Cert). setExpiration(expiration). signWith(SignatureAlgorithm.RS512, key). compact() Get(s"/fromDashboard/ping") ~> addCredentials(OAuth2BearerToken(jwtsString)) ~> sealRoute(route) ~> check { assertResult(Unauthorized)(status) } } "DashboardService" should "reject a fromDashboard/ping with no Authorization header" in { Get(s"/fromDashboard/ping") ~> sealRoute(route) ~> check { assertResult(Unauthorized)(status) } } "DashboardService" should "reject a fromDashboard/ping with an Authorization header for the wrong authorization spec" in { Get(s"/fromDashboard/ping") ~> addCredentials(adminCredentials) ~> sealRoute(route) ~> check { assertResult(Unauthorized)(status) } } "DashboardService" should "not find a bogus web service to talk to" in { Get(s"/toDashboard/bogus.harvard.edu/ping") ~> addCredentials(adminCredentials) ~> sealRoute(route) ~> check { val string = new String(body.data.toByteArray) assertResult(NotFound)(status) } } } diff --git a/apps/shrine-app/src/test/resources/shrine.conf b/apps/shrine-app/src/test/resources/shrine.conf index 8a36ecb0c..8b035c836 100644 --- a/apps/shrine-app/src/test/resources/shrine.conf +++ b/apps/shrine-app/src/test/resources/shrine.conf @@ -1,90 +1,89 @@ shrine { problem { problemHandler = "net.shrine.problem.NoOpProblemHandler$" } ontEndpoint { url = "http://example.com:9090/i2b2/rest/OntologyService/" acceptAllCerts = true timeout { seconds = 1 } } keystore { file = "shrine.keystore" - password = "chiptesting" - privateKeyAlias = "test-cert" + password = "justatestpassword" keyStoreType = "JKS" - caCertAliases = [carra ca] + trustModelIsHub = true } queryEntryPoint { audit { collectQepAudit = false database { slickProfileClassName = "slick.driver.H2Driver$" createTablesOnStart = true //for testing with H2 in memory, when not running unit tests. Set to false normally dataSourceFrom = "testDataSource" //Can be JNDI or testDataSource . Use testDataSource for tests, JNDI everywhere else testDataSource { driverClassName = "org.h2.Driver" url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" //H2 embedded in-memory for unit tests ;TRACE_LEVEL_SYSTEM_OUT=2 for H2's trace } } } trustModelIsHub = true authenticationType = "pm" //can be none, pm, or ecommons authorizationType = "shrine-steward" //can be none, shrine-steward, or hms-steward shrineSteward { qepUserName = "qep" qepPassword = "trustme" stewardBaseUrl = "https://localhost:6443" } } adapter { create = true audit { collectQepAudit = false database { slickProfileClassName = "slick.driver.H2Driver$" createTablesOnStart = true //for testing with H2 in memory, when not running unit tests. Set to false normally dataSourceFrom = "testDataSource" //Can be JNDI or testDataSource . Use testDataSource for tests, JNDI everywhere else testDataSource { driverClassName = "org.h2.Driver" url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" //H2 embedded in-memory for unit tests ;TRACE_LEVEL_SYSTEM_OUT=2 for H2's trace } } } } squerylDataSource { database { dataSourceFrom = "testDataSource" //Can be JNDI or testDataSource . Use testDataSource for tests, JNDI everywhere else testDataSource { driverClassName = "org.h2.Driver" url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" //H2 embedded in-memory for unit tests ;TRACE_LEVEL_SYSTEM_OUT=2 for H2's trace } } } // squerylDataSource { // database { // dataSourceFrom = "testDataSource" //Can be JNDI or testDataSource . Use testDataSource for tests, JNDI everywhere else // // testDataSource { // driverClassName = "org.h2.Driver" // url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1" //H2 embedded in-memory for unit tests ;TRACE_LEVEL_SYSTEM_OUT=2 for H2's trace // } // } // } } \ No newline at end of file diff --git a/apps/shrine-app/src/test/resources/shrine.keystore b/apps/shrine-app/src/test/resources/shrine.keystore index faa5158ea..918effcbc 100644 Binary files a/apps/shrine-app/src/test/resources/shrine.keystore and b/apps/shrine-app/src/test/resources/shrine.keystore differ diff --git a/commons/auth/src/main/scala/net/shrine/authentication/UserAuthenticator.scala b/commons/auth/src/main/scala/net/shrine/authentication/UserAuthenticator.scala index 519698ee3..21ff50197 100644 --- a/commons/auth/src/main/scala/net/shrine/authentication/UserAuthenticator.scala +++ b/commons/auth/src/main/scala/net/shrine/authentication/UserAuthenticator.scala @@ -1,165 +1,167 @@ package net.shrine.authentication import com.typesafe.config.Config import net.shrine.authorization.steward.{qepRole, stewardRole} import net.shrine.client.{EndpointConfig, JerseyHttpClient, Poster} +import net.shrine.crypto.TrustParam.BouncyKeyStore import net.shrine.crypto.{KeyStoreCertCollection, KeyStoreDescriptor, KeyStoreDescriptorParser, TrustParam} +import net.shrine.crypto2.BouncyKeyStoreCollection import net.shrine.i2b2.protocol.pm.{BadUsernameOrPasswordException, GetUserConfigurationRequest, PmUserWithoutProjectException, User} import net.shrine.log.Loggable import net.shrine.protocol.{AuthenticationInfo, Credential} import spray.http.HttpChallenge import spray.http.HttpHeaders.`WWW-Authenticate` import spray.routing.AuthenticationFailedRejection.CredentialsRejected import spray.routing.{AuthenticationFailedRejection, RejectionError} import spray.routing.authentication.{BasicAuth, UserPass} import spray.routing.directives.AuthMagnet import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{ExecutionContext, Future, blocking} /** * @author david * @since 3/16/15 */ trait UserSource { def authenticateUser(userPass: Option[UserPass]): Future[Option[User]] val challengeHeader:`WWW-Authenticate` = `WWW-Authenticate`(HttpChallenge("BasicCustom", "Realm")) // val challengeHeader:`WWW-Authenticate` = `WWW-Authenticate`(HttpChallenge("Basic", "Realm")) } //See http://www.tecnoguru.com/blog/2014/07/07/implementing-http-basic-authentication-with-spray/ for an explanation case class UserAuthenticator(config:Config) extends Loggable { val userSource:UserSource = config.getString("shrine.authenticate.usersource.type") match { case PmUserSource.configName => PmUserSource(config) case ConfigUserSource.configName => ConfigUserSource(config) case x => throw new IllegalStateException(s"The config value for 'shrine.authenticate.usersource.type' must be either ${PmUserSource.configName} for actual use or ${ConfigUserSource.configName} for testing. '$x' cannot be used.") } val realm = config.getString("shrine.authenticate.realm") def basicUserAuthenticator(implicit ec: ExecutionContext): AuthMagnet[User] = { def authenticator(userPass: Option[UserPass]): Future[Option[User]] = userSource.authenticateUser(userPass) BasicAuth((a:Option[UserPass]) => authenticator(a), realm = realm)(ec) } } case class PmUserSource(config:Config) extends UserSource with Loggable { val domain = config.getString("shrine.authenticate.usersource.domain") def authenticateUser(userPassOption: Option[UserPass]): Future[Option[User]] = Future { val noUser:Option[User] = None userPassOption.fold(noUser)(userPass => { val requestString = GetUserConfigurationRequest(AuthenticationInfo(domain, username = userPass.user, credential = Credential(userPass.pass, isToken = false))).toI2b2String val httpResponse = blocking { pmPoster.post(requestString) } if (httpResponse.statusCode >= 400) { val message = s"HttpResponse status is ${httpResponse.statusCode} from PM via ${pmPoster.url} for ${userPass.user}." // todo log Full response is ${httpResponse}" warn(message) throw new IllegalStateException(message) } try { val user = User.fromI2b2(httpResponse.body).get Some(user) } catch { case x: BadUsernameOrPasswordException => { info(s"PM at ${pmPoster.url} found no (user,password) combination for ${userPass.user}.", x) throw RejectionError(AuthenticationFailedRejection(CredentialsRejected, List(challengeHeader))) } case x: PmUserWithoutProjectException => { warn(s"PM at ${pmPoster.url} found no projects for ${userPass.user}.", x) throw RejectionError(AuthenticationFailedRejection(CredentialsRejected, List(challengeHeader))) } } } )} lazy val pmPoster: Poster = { val pmEndpoint: EndpointConfig = EndpointConfig(config.getConfig("shrine.pmEndpoint")) import TrustParam.{AcceptAllCerts, SomeKeyStore} val trustParam = if (pmEndpoint.acceptAllCerts) AcceptAllCerts else { val keyStoreDescriptor:KeyStoreDescriptor = KeyStoreDescriptorParser.apply(config.getConfig("shrine.keystore")) - val keystoreCertCollection: KeyStoreCertCollection = KeyStoreCertCollection.fromFileRecoverWithClassPath(keyStoreDescriptor) - SomeKeyStore(keystoreCertCollection) + val keystoreCertCollection: BouncyKeyStoreCollection = BouncyKeyStoreCollection.fromFileRecoverWithClassPath(keyStoreDescriptor) + BouncyKeyStore(keystoreCertCollection) } val httpClient = JerseyHttpClient(trustParam, pmEndpoint.timeout) Poster(pmEndpoint.url.toString, httpClient) } } object PmUserSource { val configName = getClass.getSimpleName.dropRight(1) } case class ConfigUserSource(config:Config) extends UserSource { val prefix = "shrine.authenticate.usersource" val subconfig = config.getConfig(prefix) import scala.collection.JavaConverters._ val roles = subconfig.entrySet().asScala. filterNot(x => x.getKey == "type"). filterNot(x => x.getKey == "domain"). map(x => x.getKey.take(x.getKey.indexOf("."))) //x:Entry[String,ConfigValue] val rolesToUserNames: Map[String, String] = roles.map(x => ( x, subconfig.getConfig(x).getString("username") )).toMap val userNamesToPasswords: Map[String, String] = rolesToUserNames.map(x => ( x._2, subconfig.getConfig(x._1).getString("password") )) lazy val qepUserName = rolesToUserNames("qep") lazy val qepPassword = userNamesToPasswords(qepUserName) lazy val researcherUserName = rolesToUserNames("researcher") lazy val researcherPassword = userNamesToPasswords(researcherUserName) lazy val stewardUserName = rolesToUserNames("steward") lazy val stewardPassword = userNamesToPasswords(stewardUserName) def authenticateUser(userPass: Option[UserPass]): Future[Option[User]] = Future { val noUser:Option[User] = None userPass.fold(noUser)(up =>{ if (userNamesToPasswords(up.user) == up.pass) { val params:Map[String,String] = if (up.user == "qep") Map(qepRole -> "true") else if (up.user == stewardUserName) Map(stewardRole -> "true") else Map.empty val user: User = User(fullName = up.user, username = up.user, domain = "domain", credential = Credential(up.pass,isToken = false), params = params, rolesByProject = Map()) Some(user) } else throw RejectionError(AuthenticationFailedRejection(CredentialsRejected,List(challengeHeader))) }) } } object ConfigUserSource { val configName = getClass.getSimpleName.dropRight(1) } \ No newline at end of file diff --git a/commons/crypto/src/main/scala/net/shrine/crypto/KeyStoreDescriptor.scala b/commons/crypto/src/main/scala/net/shrine/crypto/KeyStoreDescriptor.scala index b40ac1ec0..eae18f8bd 100644 --- a/commons/crypto/src/main/scala/net/shrine/crypto/KeyStoreDescriptor.scala +++ b/commons/crypto/src/main/scala/net/shrine/crypto/KeyStoreDescriptor.scala @@ -1,20 +1,20 @@ package net.shrine.crypto -import net.shrine.util.{SingleHubModel, TrustModel} +import net.shrine.util.{PeerToPeerModel, SingleHubModel, TrustModel} /** * @author clint * @since Nov 22, 2013 */ //todo consolidate with KeyStoreParser, maybe combine the whole works into KeyStoreCertCollection's collection final case class KeyStoreDescriptor( file: String, password: String, privateKeyAlias: Option[String], caCertAliases: Seq[String], keyStoreType: KeyStoreType = KeyStoreType.Default, trustModel: TrustModel = SingleHubModel) { //TODO: MAKE SURE THIS IS GOOD TO GO override def toString = scala.runtime.ScalaRunTime._toString(this.copy(password = "REDACTED")) } \ No newline at end of file diff --git a/commons/crypto/src/main/scala/net/shrine/crypto2/BouncyKeyStoreCollection.scala b/commons/crypto/src/main/scala/net/shrine/crypto2/BouncyKeyStoreCollection.scala index 0e14ffb8c..ab2f19e76 100644 --- a/commons/crypto/src/main/scala/net/shrine/crypto2/BouncyKeyStoreCollection.scala +++ b/commons/crypto/src/main/scala/net/shrine/crypto2/BouncyKeyStoreCollection.scala @@ -1,135 +1,136 @@ package net.shrine.crypto2 import java.io.{File, FileInputStream} import java.math.BigInteger import java.security.cert.X509Certificate import java.security.{KeyStore, PrivateKey, Security} import java.time.Instant import java.util.Date import javax.xml.datatype.XMLGregorianCalendar import net.shrine.crypto._ import net.shrine.log.Loggable import net.shrine.protocol.{BroadcastMessage, CertId, Signature} import net.shrine.util._ +import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.jce.provider.BouncyCastleProvider import scala.concurrent.duration.Duration /** * Created by ty on 10/25/16. * * Rewrite of [[net.shrine.crypto.CertCollection]]. Abstracts away the need to track down * all the corresponding pieces of a KeyStore entry by collecting them into a collection * of [[KeyStoreEntry]]s. * See: [[HubCertCollection]], [[PeerCertCollection]], [[CertCollectionAdapter]] */ trait BouncyKeyStoreCollection extends Loggable { val myEntry: KeyStoreEntry def signBytes(bytesToSign: Array[Byte]): Array[Byte] = myEntry.sign(bytesToSign).getOrElse(CryptoErrors.noKeyError(myEntry)) def verifyBytes(signedBytes: Array[Byte], signatureBytes: Array[Byte]): Boolean def allEntries: Iterable[KeyStoreEntry] def keyStore: KeyStore = BouncyKeyStoreCollection.keyStore.getOrElse(throw new IllegalStateException("Accessing keyStore without loading from keyStore file first!")) def descriptor: KeyStoreDescriptor = BouncyKeyStoreCollection.descriptor.getOrElse(throw new IllegalStateException("Accessing keyStoreDescriptor without loading from keyStore file first!")) } /** * Factory object that reads the correct cert collection from the file. */ object BouncyKeyStoreCollection extends Loggable { import scala.collection.JavaConversions._ import CryptoErrors._ Security.addProvider(new BouncyCastleProvider()) var descriptor: Option[KeyStoreDescriptor] = None var keyStore: Option[KeyStore] = None // On failure creates a problem so it gets logged into the database. type EitherCertError = Either[ImproperlyConfiguredKeyStoreProblem, BouncyKeyStoreCollection] /** * Creates a cert collection from a keyStore. Returns an Either to abstract away * try catches/problem construction until the end. * @return [[EitherCertError]] */ def createCertCollection(keyStore: KeyStore, descriptor: KeyStoreDescriptor): EitherCertError = { // Read all of the KeyStore entries from the file into a KeyStore Entry val values = keyStore.aliases().map(alias => (alias, keyStore.getCertificate(alias), Option(keyStore.getKey(alias, descriptor.password.toCharArray).asInstanceOf[PrivateKey]))) val entries = values.map(value => KeyStoreEntry(value._2.asInstanceOf[X509Certificate], NonEmptySeq(value._1, Nil), value._3)).toSet if (entries.exists(_.isExpired())) Left(configureError(ExpiredCertificates(entries.filter(_.isExpired())))) else descriptor.trustModel match { case PeerToPeerModel => createPeerCertCollection(entries, descriptor, keyStore) case SingleHubModel => createHubCertCollection(entries, descriptor, keyStore) } } /** * @return a [[scala.util.Left]] if we can't find or disambiguate a [[PrivateKey]], * otherwise return [[scala.util.Right]] that contains correct [[PeerCertCollection]] */ def createPeerCertCollection(entries: Set[KeyStoreEntry], descriptor: KeyStoreDescriptor, keyStore: KeyStore): EitherCertError = { if (descriptor.caCertAliases.nonEmpty) warn(s"Specifying caCertAliases in a PeerToPeer network is useless, certs found: `${descriptor.caCertAliases}`") (descriptor.privateKeyAlias, entries.filter(_.privateKey.isDefined)) match { case (_, empty) if empty.isEmpty => Left(configureError(NoPrivateKeyInStore)) case (None, keys) if keys.size == 1 => warn(s"No private key specified, using the only entry with a private key: `${keys.head.aliases.first}`") Right(PeerCertCollection(keys.head, entries -- keys)) case (None, keys) => Left(configureError(TooManyPrivateKeys(entries))) case (Some(alias), keys) if keys.exists(_.aliases.contains(alias)) => val privateKeyEntry = keys.find(_.aliases.contains(alias)).get Right(PeerCertCollection(privateKeyEntry, entries - privateKeyEntry)) case (Some(alias), keys) => Left(configureError(CouldNotFindAlias(alias))) } } def createHubCertCollection(entries: Set[KeyStoreEntry], descriptor: KeyStoreDescriptor, keyStore: KeyStore): EitherCertError = { if (entries.size != 2) Left(configureError(RequiresExactlyTwoEntries(entries))) else if (entries.count(_.privateKey.isDefined) != 1) Left(configureError(RequiresExactlyOnePrivateKey(entries.filter(_.privateKey.isDefined)))) else { val partition = entries.partition(_.privateKey.isDefined) val privateEntry = partition._1.head val caEntry = partition._2.head if (privateEntry.wasSignedBy(caEntry)) Right(HubCertCollection(privateEntry, caEntry)) else Left(configureError(NotSignedByCa(privateEntry, caEntry))) } } //TODO: Move fromStreamHelper to crypto2 def fromFileRecoverWithClassPath(descriptor: KeyStoreDescriptor): BouncyKeyStoreCollection = { val keyStore = if (new File(descriptor.file).exists) KeyStoreCertCollection.fromStreamHelper(descriptor, new FileInputStream(_)) else KeyStoreCertCollection.fromStreamHelper(descriptor, getClass.getClassLoader.getResourceAsStream(_)) BouncyKeyStoreCollection.keyStore = Some(keyStore) BouncyKeyStoreCollection.descriptor = Some(descriptor) createCertCollection(keyStore, descriptor) .fold(problem => throw problem.throwable.get, identity) } } \ No newline at end of file diff --git a/commons/crypto/src/main/scala/net/shrine/crypto2/KeyStoreEntry.scala b/commons/crypto/src/main/scala/net/shrine/crypto2/KeyStoreEntry.scala index 9f06f782a..5d591f416 100644 --- a/commons/crypto/src/main/scala/net/shrine/crypto2/KeyStoreEntry.scala +++ b/commons/crypto/src/main/scala/net/shrine/crypto2/KeyStoreEntry.scala @@ -1,97 +1,122 @@ package net.shrine.crypto2 import java.security.cert.X509Certificate import java.security._ import java.time.{Clock, Instant} +import java.util import java.util.Date import net.shrine.crypto.UtilHasher import net.shrine.util.NonEmptySeq import org.bouncycastle.asn1.x500.style.{BCStyle, IETFUtils} import org.bouncycastle.asn1.x509.AlgorithmIdentifier import org.bouncycastle.cert.X509CertificateHolder -import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder +import org.bouncycastle.cert.jcajce.{JcaCertStore, JcaX509CertificateConverter, JcaX509CertificateHolder} import org.bouncycastle.cms._ import org.bouncycastle.cms.jcajce.{JcaSignerInfoGeneratorBuilder, JcaSimpleSignerInfoVerifierBuilder} import org.bouncycastle.jce.provider.BouncyCastleProvider -import org.bouncycastle.operator.ContentSigner +import org.bouncycastle.operator.{ContentSigner, ContentVerifier} import org.bouncycastle.operator.bc.BcDigestCalculatorProvider import org.bouncycastle.operator.jcajce.{JcaContentSignerBuilder, JcaContentVerifierProviderBuilder, JcaDigestCalculatorProviderBuilder} import org.bouncycastle.util.{Selector, Store} import scala.util.Try /** * Created by ty on 10/26/16. * Represents a single entry in a key store collection. As a key entry may be either a PrivateKey Entry * or a TrustedCert Entry, there's no guarantee that there is a privateKey available * * @param cert: The x509 certificate in the entry * @param aliases: The alias of the certificate in the keystore * @param privateKey: The private key of the certificate, which is only available if this keystore represents * a private key entry (i.e., do we own this certificate?) */ final case class KeyStoreEntry(cert: X509Certificate, aliases: NonEmptySeq[String], privateKey: Option[PrivateKey]) { val publicKey:PublicKey = cert.getPublicKey val certificateHolder = new JcaX509CertificateHolder(cert) // Helpful methods are defined in the cert holder. val isSelfSigned: Boolean = certificateHolder.getSubject == certificateHolder.getIssuer // May or may not be a CA val formattedSha256Hash: String = UtilHasher.encodeCert(cert, "SHA-256") - val commonName: Option[String] = for { // Who doesn't put CNs on their certs, I mean really - rdn <- certificateHolder.getSubject.getRDNs(BCStyle.CN).headOption - cn <- Option(rdn.getFirst) - } yield IETFUtils.valueToString(cn.getValue) + val commonName: Option[String] = KeyStoreEntry.getCommonNameFromCert(certificateHolder) // certificateHolder.getSubject.getRDNs(BCStyle.CN).headOption.flatMap(rdn => // Option(rdn.getFirst).map(cn => IETFUtils.valueToString(cn.getValue))) private val provider = new BouncyCastleProvider() def verify(signedBytes: Array[Byte], originalMessage: Array[Byte]): Boolean = { - import scala.collection.JavaConversions._ // Treat Java Iterable as Scala Iterable - val parser = new CMSSignedDataParser(new JcaDigestCalculatorProviderBuilder().setProvider(provider).build(), signedBytes) - parser.getSignedContent.drain() - - val maybeResult = for { - signerInfo <- parser.getSignerInfos.headOption - certHolder <- parser.getCertificates.asInstanceOf[Store[X509CertificateHolder]].getMatches(new Selector[X509CertificateHolder] { - override def `match`(x: X509CertificateHolder): Boolean = true - }).headOption - verifier = new JcaContentVerifierProviderBuilder().setProvider(provider).build(certificateHolder) - } yield certHolder.isSignatureValid(verifier) - - maybeResult.exists(identity) + KeyStoreEntry.extractCertHolder(signedBytes).exists(_.isSignatureValid( + new JcaContentVerifierProviderBuilder() + .setProvider(provider).build(certificateHolder) + ) + ) } /** * Provided that this is a PrivateKey Entry, sign the incoming bytes. * @return Returns None if this is not a PrivateKey Entry */ def sign(bytesToSign: Array[Byte]): Option[Array[Byte]] = { + import scala.collection.JavaConversions._ + val SHA256 = "SHA256withRSA" privateKey.map(key => { + val signature = Signature.getInstance(SHA256, provider) + signature.initSign(key) + signature.update(bytesToSign) + + val data = new CMSProcessableByteArray(signature.sign()) val gen = new CMSSignedDataGenerator() - val contentSigner: ContentSigner = new JcaContentSignerBuilder("SHA256withRSA").setProvider(provider).build(key) - val builder = new JcaSignerInfoGeneratorBuilder(new JcaDigestCalculatorProviderBuilder().setProvider(provider).build) - contentSigner.getOutputStream.write(bytesToSign) - contentSigner.getOutputStream.flush() - val msg = new CMSProcessableByteArray(contentSigner.getSignature) - builder.setDirectSignature(true) - gen.addSignerInfoGenerator(builder.build(contentSigner, cert)) - gen.addCertificate(certificateHolder) - gen.generate(msg, true).getEncoded + + gen.addCertificates(new JcaCertStore(Seq(cert))) + val result = gen.generate(data, true).getEncoded + result }) } def wasSignedBy(entry: KeyStoreEntry): Boolean = wasSignedBy(entry.publicKey) def wasSignedBy(publicKey: PublicKey): Boolean = certificateHolder.isSignatureValid( - new JcaContentVerifierProviderBuilder().setProvider("BC").build(publicKey) + new JcaContentVerifierProviderBuilder().setProvider(provider).build(publicKey) + ) + + def signed(cert: X509Certificate): Boolean = new JcaX509CertificateHolder(cert).isSignatureValid( + new JcaContentVerifierProviderBuilder().setProvider(provider).build(publicKey) ) + def isExpired(clock: Clock = Clock.systemDefaultZone()): Boolean = { certificateHolder.getNotAfter.before(Date.from(Instant.now(clock))) } +} + +object KeyStoreEntry { + def extractCertHolder(signedBytes: Array[Byte]): Option[X509CertificateHolder] = { + import scala.collection.JavaConversions._ + val signedData = new CMSSignedData(signedBytes) + val store = signedData.getCertificates.asInstanceOf[Store[X509CertificateHolder]] + val certCollection = store.getMatches(SelectAll) + certCollection.headOption + } + + def getCommonNameFromCert(certHolder: X509CertificateHolder): Option[String] = { + for { + rdn <- certHolder.getSubject.getRDNs(BCStyle.CN).headOption + cn <- Option(rdn.getFirst) + } yield IETFUtils.valueToString(cn.getValue) + } + + def extractCommonName(signedBytes: Array[Byte]): Option[String] = { + for { + certHolder <- extractCertHolder(signedBytes) + commonName <- getCommonNameFromCert(certHolder) + } yield commonName + } +} + +object SelectAll extends Selector[X509CertificateHolder] { + override def `match`(obj: X509CertificateHolder): Boolean = true } \ No newline at end of file diff --git a/commons/crypto/src/main/scala/net/shrine/crypto2/SignerVerifierAdapter.scala b/commons/crypto/src/main/scala/net/shrine/crypto2/SignerVerifierAdapter.scala index e297f8ff1..006c144b8 100644 --- a/commons/crypto/src/main/scala/net/shrine/crypto2/SignerVerifierAdapter.scala +++ b/commons/crypto/src/main/scala/net/shrine/crypto2/SignerVerifierAdapter.scala @@ -1,81 +1,79 @@ package net.shrine.crypto2 import java.math.BigInteger import javax.xml.datatype.XMLGregorianCalendar import net.shrine.crypto.{Signer, SigningCertStrategy, Verifier} import net.shrine.protocol.{BroadcastMessage, CertId, Signature} import net.shrine.util.{XmlDateHelper, XmlGcEnrichments} import scala.concurrent.duration.Duration /** * An adapter object so that the new crypto package can coexist with the * existing Signer and Verifier interfaces * @param keyStoreCollection The BouncyKeyStoreCollection that is signing * and verifying broadcast messages */ case class SignerVerifierAdapter(keyStoreCollection: BouncyKeyStoreCollection) extends BouncyKeyStoreCollection with Signer with Verifier { override def signBytes(bytesToSign: Array[Byte]): Array[Byte] = keyStoreCollection.signBytes(bytesToSign) override def verifyBytes(signedBytes: Array[Byte], signatureBytes: Array[Byte]): Boolean = keyStoreCollection.verifyBytes(signedBytes, signatureBytes) override val myEntry: KeyStoreEntry = keyStoreCollection.myEntry override def allEntries: Iterable[KeyStoreEntry] = keyStoreCollection.allEntries override def sign(message: BroadcastMessage, signingCertStrategy: SigningCertStrategy): BroadcastMessage = { val certAdapter = CertCollectionAdapter(keyStoreCollection) val timeStamp = XmlDateHelper.now val dummyCertId = certAdapter.myCertId.get val signedBytes = signBytes(toBytes(message, timeStamp)) val sig = Signature(timeStamp, dummyCertId, None, signedBytes) message.withSignature(sig) } override def verifySig(message: BroadcastMessage, maxSignatureAge: Duration): Boolean = { val logSigFailure = (b:Boolean) => { if (!b) { UnknownSignatureProblem(message) warn(s"Error verifying signature for message with id '${message.requestId}'") } b } message.signature.exists(sig => { val notTooOl = notTooOld(sig, maxSignatureAge, message) val verify = verifyBytes(sig.value.array, toBytes(message, sig.timestamp)) - println(s"\n notTooOld: $notTooOl\n") - println(s"\n verify: $verify\n") notTooOl && logSigFailure(verify) } ) } // Has the signature expired? private def notTooOld(sig: Signature, maxSignatureAge: Duration, message: BroadcastMessage): Boolean = { import XmlGcEnrichments._ val sigValidityEndTime: XMLGregorianCalendar = sig.timestamp + maxSignatureAge val now = XmlDateHelper.now val timeout = sigValidityEndTime > now if (!timeout) warn(s"Could not validate message with id '${message.requestId}' due to " + s"exceeding max timeout of $maxSignatureAge") timeout } // Concatenates with the timestamp. This is how it's converted to bytes in the // the DefaultSignerVerifier, but now that we're using CMS I don't think this is necessary // anymore. It was only done before to ensure unique signatures, I believe. private def toBytes(message: BroadcastMessage, timestamp: XMLGregorianCalendar): Array[Byte] = { val messageXml = message.copy(signature = None).toXmlString val timestampXml = timestamp.toXMLFormat (messageXml + timestampXml).getBytes("UTF-8") } } \ No newline at end of file diff --git a/commons/crypto/src/test/scala/net/shrine/crypto2/HubCertCollectionTest.scala b/commons/crypto/src/test/scala/net/shrine/crypto2/HubCertCollectionTest.scala index 23f14a0d7..653fd124b 100644 --- a/commons/crypto/src/test/scala/net/shrine/crypto2/HubCertCollectionTest.scala +++ b/commons/crypto/src/test/scala/net/shrine/crypto2/HubCertCollectionTest.scala @@ -1,36 +1,80 @@ package net.shrine.crypto2 +import java.math.BigInteger +import java.security.cert.X509Certificate +import java.security.{KeyPairGenerator, SecureRandom} +import java.util.Date +import javax.security.auth.x500.X500Principal + import junit.framework.TestFailure import net.shrine.crypto.{KeyStoreDescriptor, KeyStoreType, NewTestKeyStore} -import net.shrine.util.SingleHubModel +import net.shrine.util.{NonEmptySeq, SingleHubModel} +import org.bouncycastle.asn1.ASN1Sequence +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509._ +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.bouncycastle.x509.X509V3CertificateGenerator import org.junit.runner.RunWith import org.scalatest.{FlatSpec, Matchers, ShouldMatchers} import org.scalatest.junit.JUnitRunner /** * Created by ty on 11/1/16. */ @RunWith(classOf[JUnitRunner]) class HubCertCollectionTest extends FlatSpec with Matchers { val descriptor = NewTestKeyStore.descriptor val heyo = "Heyo!".getBytes("UTF-8") "A hub cert collection" should "build and verify its own messages" in { val hubCertCollection = BouncyKeyStoreCollection.fromFileRecoverWithClassPath(descriptor) match { case hub:HubCertCollection => hub case _ => fail("This should generate a HubCertCollection!") } + val testEntry = KeyStoreEntry(TestCert.cert, NonEmptySeq("notTrusted", Nil), Some(TestCert.keyPair.getPrivate)) + hubCertCollection.allEntries.size shouldBe 2 hubCertCollection.myEntry.privateKey.isDefined shouldBe true hubCertCollection.caEntry.privateKey.isDefined shouldBe false hubCertCollection.myEntry.aliases.first shouldBe "shrine-test" hubCertCollection.caEntry.aliases.first shouldBe "shrine-test-ca" hubCertCollection.caEntry.wasSignedBy(hubCertCollection.myEntry) shouldBe false hubCertCollection.myEntry.wasSignedBy(hubCertCollection.caEntry) shouldBe true - //hubCertCollection.myEntry.verify(hubCertCollection.myEntry.sign(heyo).get, heyo) shouldBe true - hubCertCollection.caEntry.verify(hubCertCollection.myEntry.sign(heyo).get, heyo) shouldBe true + + val mySigned = hubCertCollection.myEntry.sign(heyo).get + val testSigned = testEntry.sign(heyo).get + + testEntry.verify(mySigned, heyo) shouldBe false + testEntry.verify(testSigned, heyo) shouldBe true + + hubCertCollection.myEntry.verify(testSigned, heyo) shouldBe false +// hubCertCollection.myEntry.verify(mySigned, heyo) shouldBe true + + hubCertCollection.caEntry.verify(testSigned, heyo) shouldBe false + hubCertCollection.caEntry.verify(mySigned, heyo) shouldBe true + hubCertCollection.verifyBytes(hubCertCollection.signBytes(heyo), heyo) shouldBe true } } + +object TestCert { + // generate a key pair + val keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC") + keyPairGenerator.initialize(4096, new SecureRandom()) + + val keyPair = keyPairGenerator.generateKeyPair() + val name: X500Name = new X500Name("cn=testing") + val subject = new X500Name("dc=stillTesting") + val serial = BigInteger.valueOf(System.currentTimeMillis()) + val notBefore = new Date(0) + val notAfter = new Date(java.time.Instant.now().toEpochMilli + 1000 * 60 * 60 * 24 * 10) + val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(keyPair.getPublic.getEncoded)) + + val certBuilder = new X509v3CertificateBuilder(name, serial, notBefore, notAfter, subject, subjectPublicKeyInfo) + val certHolder = certBuilder.build(new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate)) + val cert = new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder) +} diff --git a/commons/util/src/main/scala/net/shrine/problem/Problem.scala b/commons/util/src/main/scala/net/shrine/problem/Problem.scala index 694b65c28..be2d15642 100644 --- a/commons/util/src/main/scala/net/shrine/problem/Problem.scala +++ b/commons/util/src/main/scala/net/shrine/problem/Problem.scala @@ -1,226 +1,232 @@ package net.shrine.problem import java.net.InetAddress import java.util.Date import java.util.concurrent.Executors import net.shrine.log.Loggable import net.shrine.serialization.{XmlMarshaller, XmlUnmarshaller} import net.shrine.slick.NeedsWarmUp import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{ExecutionContext, Future, Promise} import scala.util.Try import scala.xml.{Elem, Node, NodeSeq} /** * Describes what information we have about a problem at the site in code where we discover it. * * @author david * @since 8/6/15 */ trait Problem { def summary:String def problemName = getClass.getName def throwable:Option[Throwable] = None def stamp:Stamp def description:String def exceptionXml(exception:Option[Throwable]): Option[Elem] = { exception.map{x => {x.getClass.getName} {x.getMessage} {x.getStackTrace.map(line => {line})}{exceptionXml(Option(x.getCause)).getOrElse("")} }} def throwableDetail: Option[Elem] = exceptionXml(throwable) def detailsXml: NodeSeq = NodeSeq.fromSeq(
{throwableDetail.getOrElse("")}
) def toDigest:ProblemDigest = ProblemDigest(problemName,stamp.pretty,summary,description,detailsXml,stamp.time) /** * Temporary replacement for onCreate, which will be released come Scala 2.13 * TODO: remove when Scala 2.13 releases */ def hackToHandleAfterInitialization(handler:ProblemHandler):Future[Unit] = { import scala.concurrent.blocking Future { var continue = true while (continue) { - continue = Try(blocking(synchronized(handler.handleProblem(this)))).isSuccess - Thread.sleep(5) + try { + blocking(synchronized(handler.handleProblem(this))) + continue = false + } catch { + case un:UninitializedFieldError => + Thread.sleep(5) + continue = true + } } Unit } } } case class ProblemDigest(codec: String, stampText: String, summary: String, description: String, detailsXml: NodeSeq, epoch: Long) extends XmlMarshaller { override def toXml: Node = { {codec} {stampText} {summary} {description} {epoch} {detailsXml} } /** * Ignores detailXml. equals with scala.xml is impossible. See http://www.scala-lang.org/api/2.10.3/index.html#scala.xml.Equality$ */ override def equals(other: Any): Boolean = other match { case that: ProblemDigest => (that canEqual this) && codec == that.codec && stampText == that.stampText && summary == that.summary && description == that.description && epoch == that.epoch case _ => false } /** * Ignores detailXml */ override def hashCode: Int = { val prime = 67 codec.hashCode + prime * (stampText.hashCode + prime *(summary.hashCode + prime * (description.hashCode + prime * epoch.hashCode()))) } } object ProblemDigest extends XmlUnmarshaller[ProblemDigest] with Loggable { override def fromXml(xml: NodeSeq): ProblemDigest = { val problemNode = xml \ "problem" require(problemNode.nonEmpty,s"No problem tag in $xml") def extractText(tagName:String) = (problemNode \ tagName).text val codec = extractText("codec") val stampText = extractText("stamp") val summary = extractText("summary") val description = extractText("description") val detailsXml: NodeSeq = problemNode \ "details" val epoch = try { extractText("epoch").toLong } catch { case nx:NumberFormatException => error(s"While parsing xml representing a ProblemDigest, the epoch could not be parsed into a long", nx) 0 } ProblemDigest(codec,stampText,summary,description,detailsXml,epoch) } } case class Stamp(host:InetAddress,time:Long,source:ProblemSources.ProblemSource) { def pretty = s"${new Date(time)} on ${host.getHostName} ${source.pretty}" } object Stamp { //TODO: val dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")? //TODO: Currently the stamp text is locale specific, which can change depending on the jre/computer running it... def apply(source:ProblemSources.ProblemSource, timer: => Long): Stamp = Stamp(InetAddress.getLocalHost, timer, source) } abstract class AbstractProblem(source:ProblemSources.ProblemSource) extends Problem { def timer = System.currentTimeMillis override val stamp = Stamp(source, timer) private val config = ProblemConfigSource.config.getConfig("shrine.problem") hackToHandleAfterInitialization(ProblemConfigSource.getObject("problemHandler", config)) } trait ProblemHandler extends NeedsWarmUp { def handleProblem(problem:Problem) } /** * An example problem handler */ object LoggingProblemHandler extends ProblemHandler with Loggable { override def handleProblem(problem: Problem): Unit = { problem.throwable.fold(error(problem.toString))(throwable => error(problem.toString,throwable) ) } override def warmUp(): Unit = Unit } object DatabaseProblemHandler extends ProblemHandler with Loggable { override def handleProblem(problem: Problem): Unit = { Problems.DatabaseConnector.insertProblem(problem.toDigest) } override def warmUp(): Unit = Problems.warmUp } /** * Mainly for testing, when you don't want problems to print a bunch * to stdout */ object NoOpProblemHandler extends ProblemHandler { override def handleProblem(problem: Problem): Unit = Unit override def warmUp(): Unit = Unit } object ProblemSources{ sealed trait ProblemSource { def pretty = getClass.getSimpleName.dropRight(1) } case object Adapter extends ProblemSource case object Commons extends ProblemSource case object Dsa extends ProblemSource case object Hub extends ProblemSource case object Qep extends ProblemSource case object Unknown extends ProblemSource def problemSources = Set(Adapter,Commons,Dsa,Hub,Qep,Unknown) } case class ProblemNotYetEncoded(internalSummary:String,t:Option[Throwable] = None) extends AbstractProblem(ProblemSources.Unknown){ override val summary = "An unanticipated problem encountered." override val throwable = { val rx = t.fold(new IllegalStateException(s"$summary"))( new IllegalStateException(s"$summary",_) ) rx.fillInStackTrace() Some(rx) } val reportedAtStackTrace = new IllegalStateException("Capture reporting stack trace.") override val description = "This problem is not yet classified in Shrine source code. Please report the details to the Shrine dev team." override val detailsXml: NodeSeq = NodeSeq.fromSeq(
{internalSummary} {throwableDetail.getOrElse("")}
) } object ProblemNotYetEncoded { def apply(summary:String,x:Throwable):ProblemNotYetEncoded = ProblemNotYetEncoded(summary,Some(x)) } \ No newline at end of file diff --git a/tools/scanner/src/main/scala/net/shrine/utilities/scanner/ScannerModule.scala b/tools/scanner/src/main/scala/net/shrine/utilities/scanner/ScannerModule.scala index b4603ec64..19bcf65fc 100644 --- a/tools/scanner/src/main/scala/net/shrine/utilities/scanner/ScannerModule.scala +++ b/tools/scanner/src/main/scala/net/shrine/utilities/scanner/ScannerModule.scala @@ -1,132 +1,134 @@ package net.shrine.utilities.scanner import java.io.File import java.io.FileInputStream + import scala.concurrent.duration.Duration import SingleThreadExecutionContext.Implicits.executionContext import net.shrine.adapter.client.RemoteAdapterClient import net.shrine.broadcaster.AdapterClientBroadcaster import net.shrine.broadcaster.BroadcastAndAggregationService import net.shrine.broadcaster.InJvmBroadcasterClient import net.shrine.broadcaster.NodeHandle import net.shrine.broadcaster.SigningBroadcastAndAggregationService import net.shrine.client.JerseyHttpClient import net.shrine.client.Poster import net.shrine.config.mappings.AdapterMappingsSource import net.shrine.config.mappings.FileSystemFormatDetectingAdapterMappingsSource import net.shrine.crypto.DefaultSignerVerifier import net.shrine.crypto.KeyStoreCertCollection import net.shrine.crypto.TrustParam.AcceptAllCerts import net.shrine.hms.authentication.EcommonsPmAuthenticator import net.shrine.ont.data.OntologyDao import net.shrine.ont.data.ShrineSqlOntologyDao import net.shrine.protocol.NodeId import net.shrine.util.Versions import net.shrine.crypto.SigningCertStrategy import net.shrine.broadcaster.dao.HubDao +import net.shrine.crypto2.{BouncyKeyStoreCollection, SignerVerifierAdapter} import net.shrine.protocol.query.QueryDefinition import net.shrine.protocol.AuthenticationInfo import net.shrine.protocol.SingleNodeResult /** * @author clint * @since Mar 6, 2013 */ final class ScannerModule(args: Seq[String]) { val commandLineProps = CommandLineScannerConfigParser(args) def showVersionToggleEnabled = commandLineProps.shouldShowVersion def showHelpToggleEnabled = commandLineProps.shouldShowHelp lazy val config = ClasspathAndCommandLineScannerConfigSource.config(commandLineProps) private lazy val scanner = { val client: ScannerClient = { //TODO: MAKE THIS CONFIGURABLE? val adapterTimeout = Duration.Inf //TODO: MAKE THIS CONFIGURABLE? val pmTimeout = Duration.Inf val poster = Poster(config.shrineUrl, JerseyHttpClient(AcceptAllCerts, adapterTimeout)) val destinations = Set(NodeHandle(NodeId(config.shrineUrl), RemoteAdapterClient(NodeId(config.shrineUrl),poster, config.breakdownTypes))) - val certCollection = KeyStoreCertCollection.fromFile(config.keystoreDescriptor) + val certCollection = BouncyKeyStoreCollection.fromFileRecoverWithClassPath(config.keystoreDescriptor) - val signer = new DefaultSignerVerifier(certCollection) + val signer = SignerVerifierAdapter(certCollection) val doesNothingHubDao: HubDao = new HubDao { override def inTransaction[T](f: => T): T = f override def logOutboundQuery(networkQueryId: Long, networkAuthn: AuthenticationInfo, queryDef: QueryDefinition): Unit = () override def logQueryResult(networkQueryId: Long, result: SingleNodeResult): Unit = () } val broadcastService: BroadcastAndAggregationService = SigningBroadcastAndAggregationService(InJvmBroadcasterClient(AdapterClientBroadcaster(destinations, doesNothingHubDao)), signer, SigningCertStrategy.Attach) val pmEndpoint = config.pmUrl val pmHttpClient = JerseyHttpClient(AcceptAllCerts, pmTimeout) val authenticator = EcommonsPmAuthenticator(Poster(pmEndpoint, pmHttpClient)) new BroadcastServiceScannerClient(config.projectId, config.authorization, broadcastService, authenticator, executionContext) } val adapterMappingsSource: AdapterMappingsSource = FileSystemFormatDetectingAdapterMappingsSource(config.adapterMappingsFile) val ontologyDao: OntologyDao = new ShrineSqlOntologyDao(new FileInputStream(config.ontologySqlFile)) new Scanner( config.maxTimeToWaitForResults, config.reScanTimeout, adapterMappingsSource, ontologyDao, client) } def scan(): ScanResults = { try { scanner.scan() } finally { SingleThreadExecutionContext.shutdown() } } } object ScannerModule { def printVersionInfo() { println(s"Shrine Scanner version: ${Versions.version}") println(s"Built on ${Versions.buildDate}") println(s"SCM branch: ${Versions.scmBranch}") println(s"SCM revision: ${Versions.scmRevision}") println() } def main(args: Array[String]) { val appName = "Shrine Scanner" val scannerModule = new ScannerModule(args) if (scannerModule.showVersionToggleEnabled) { scannerModule.commandLineProps.showVersionAndExit(appName) } if (scannerModule.showHelpToggleEnabled) { println("Usage: scanner [options]") scannerModule.commandLineProps.showHelpAndExit(appName) } val outputFile = new File(scannerModule.config.outputFile) val command = Output.to(outputFile) val scanResults = scannerModule.scan() command(scanResults) } } \ No newline at end of file