Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F63592614
DashboardService.scala
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Tue, May 21, 05:29
Size
20 KB
Mime Type
text/x-c++
Expires
Thu, May 23, 05:29 (2 d)
Engine
blob
Format
Raw Data
Handle
17792503
Attached To
R2664 SHRINE MedCo Fork
DashboardService.scala
View Options
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 = 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
}
Event Timeline
Log In to Comment