Page MenuHomec4science

DashboardService.scala
No OneTemporary

File Metadata

Created
Tue, Aug 27, 07:00

DashboardService.scala

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.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 shapeless.HNil
import spray.http.{HttpRequest, HttpResponse, StatusCodes, Uri}
import spray.httpx.Json4sSupport
import spray.routing.directives.LogEntry
import spray.routing.{AuthenticationFailedRejection, Directive0, HttpService, Rejected, Route, RouteConcatenation}
import org.json4s.{DefaultFormats, Formats}
import org.json4s.native.JsonMethods.{parse => json4sParse}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.xml.Elem
/**
* 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 Json4sSupport with Loggable {
implicit def json4sFormats: Formats = DefaultFormats
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)}
}
//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/shrine-dashboard/fromDashboard/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 {
pathPrefix("config"){getConfig}~
pathPrefix("classpath"){getClasspath}~
pathPrefix("options"){getOptions}~
pathPrefix("summary"){getSummary}
}
val statusBaseUrl = DashboardConfigSource.config.getString("shrine.dashboard.statusBaseUrl")
lazy val getConfig:Route = {
def completeSummaryRoute(httpResponse:HttpResponse,uri:Uri):Route = {
ctx => {
val config = ConfigParser(httpResponse.entity.asString)
ctx.complete(
ShrineConfig(config)
)
}
}
requestUriThenRoute(statusBaseUrl + "/config", completeSummaryRoute)
}
lazy val getClasspath:Route = {
def pullClasspathFromConfig(httpResponse:HttpResponse,uri:Uri):Route = {
ctx => {
val result = httpResponse.entity.asString
val shrineConfig = ShrineConfig(ConfigParser(result))
ctx.complete(shrineConfig)
}
}
requestUriThenRoute(statusBaseUrl + "/config",pullClasspathFromConfig)
}
lazy val getOptions:Route = {
def completeSummaryRoute(httpResponse:HttpResponse,uri:Uri):Route = {
ctx => {
val config = ConfigParser(httpResponse.entity.asString)
ctx.complete(
Options(config)
)
}
}
requestUriThenRoute(statusBaseUrl + "/config", completeSummaryRoute)
}
//todo this is the Happy summary. Rename, eventually delete when the status service can provide the same
lazy val getSummary:Route = {
val happyBaseUrl: String = DashboardConfigSource.config.getString("shrine.dashboard.happyBaseUrl")
def pullSummaryFromHappy(httpResponse:HttpResponse,uri:Uri):Route = {
ctx => {
val result: Elem = scala.xml.XML.loadString(httpResponse.entity.asString)
val isHub: Boolean = (result \\ "notAHub").text.length == 0
val shrineVersion = (result \\ "versionInfo" \ "shrineVersion").text
val shrineBuildDate = (result \\ "versionInfo" \ "buildDate").text
val ontologyVersion = (result \\ "versionInfo" \ "ontologyVersion").text
val ontologyTerm = (result \\ "adapter" \\ "queryDefinition" \\ "term").text
val hubOk = {
if(!isHub) true
else {
val hasFailures = (result \\ "net" \ "failureCount").text.toInt > 0
val hasInvalidResults = (result \\ "net" \ "validResultCount").text.toInt != (result \\ "net" \ "expectedResultCount").text.toInt
val hasTimeouts = (result \\ "net" \ "timeoutCount").text.toInt > 0
!hasFailures && !hasInvalidResults && !hasTimeouts
}
}
val adapterOk = (result \\ "adapter" \\ "errorResponse").length == 0
val keystoreOk = true
val qepOk = true
val summary = Summary(
isHub = isHub,
shrineVersion = shrineVersion,
shrineBuildDate = shrineBuildDate,
ontologyVersion = ontologyVersion,
ontologyTerm = ontologyTerm,
adapterOk = adapterOk,
keystoreOk = keystoreOk,
hubOk = hubOk,
qepOk = qepOk
)
ctx.complete(summary)
}
}
requestUriThenRoute(happyBaseUrl+"/all", pullSummaryFromHappy)
}
}
case class Summary(
isHub:Boolean,
shrineVersion:String,
shrineBuildDate:String,
ontologyVersion:String,
ontologyTerm:String,
adapterOk:Boolean,
keystoreOk:Boolean,
hubOk:Boolean,
qepOk:Boolean
)
/**
* Centralized parsing logic for map of shrine.conf
* the class literal `T.class` in Java.
*/
//todo this thing is ShrineConfig's apply method. Move it all there and get rid of this mess
//todo this class' crap is spread out everywhere. Clean it all up.
case class ConfigParser(shrineMap:Map[String, String]){
private val trueVal = "true"
private val rootKey = "shrine"
def IsHub =
getOrElse(rootKey + ".hub.create", "")
.toLowerCase == trueVal
// -- -- //
def StewardEnabled =
shrineMap.keySet
.contains(rootKey + ".queryEntryPoint.shrineSteward")
// -- -- //
def ShouldQuerySelf =
getOrElse(rootKey + ".hub.shouldQuerySelf", "")
.toLowerCase == trueVal
// -- -- //
def DownstreamNodes =
for((k,v) <- shrineMap.filterKeys(_.toLowerCase.startsWith
("shrine.hub.downstreamnodes"))) yield DownstreamNode(k.split('.').last,
v.split("\"").mkString(""))
// -- -- //
def fromJsonString(jsonString:String) = jsonString.split("\"").mkString("")
def get(key:String) = shrineMap.get(key).map(fromJsonString)
def getOrElse(key:String, elseVal:String = "") = get(key).getOrElse(elseVal)
}
object ConfigParser {
def apply(jsonString:String):ConfigParser = {
// -- needed to use json4s parse -- //
implicit def json4sFormats: Formats = DefaultFormats
ConfigParser(json4sParse(jsonString).extract[StatusProtocolConfig].keyValues
.filterKeys(_.toLowerCase.startsWith("shrine")))
}
}
case class DownstreamNode(name:String, url:String){
}
//todo this is filling for the dashboard Summary - major components of shrine, and downstream nodes for a hub. Rename
case class Options(isHub:Boolean, stewardEnabled:Boolean, shouldQuerySelf:Boolean,
downstreamNodes:Iterable[DownstreamNode])
object Options{
def apply(parsedConfig:ConfigParser):Options ={
val isHub = parsedConfig.IsHub
val stewardEnabled = parsedConfig.StewardEnabled
val shouldQuerySelf = parsedConfig.ShouldQuerySelf
val downstreamNodes = parsedConfig.DownstreamNodes
Options(isHub, stewardEnabled, shouldQuerySelf, downstreamNodes)
}
}
//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
)
object ShrineConfig{
def apply(config:ConfigParser):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.shrineMap("shrine.networkStatusQuery")
ShrineConfig(isHub, hub, pmEndpoint, ontEndpoint, hiveCredentials, adapter, queryEntryPoint, networkStatusQuery)
}
}
case class Endpoint(acceptAllCerts:Boolean, url:String, timeoutSeconds:Int)
object Endpoint{
def apply(endpointType:String,parsedConfig:ConfigParser):Endpoint = {
val prefix = "shrine." + endpointType.toLowerCase + "Endpoint."
val acceptAllCerts = parsedConfig.shrineMap.getOrElse(prefix + "acceptAllCerts", "") == "true"
val url = parsedConfig.shrineMap.getOrElse(prefix + "url","")
val timeoutSeconds = parsedConfig.shrineMap.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:ConfigParser):HiveCredentials = {
val key = "shrine.hiveCredentials."
val domain = parsedConfig.shrineMap.getOrElse(key + "domain","")
val username = parsedConfig.shrineMap.getOrElse(key + "username","")
val password = "REDACTED"
val crcProjectId = parsedConfig.shrineMap.getOrElse(key + "crcProjectId","")
val ontProjectId = parsedConfig.shrineMap.getOrElse(key + "ontProjectId","")
HiveCredentials(domain, username, password, crcProjectId, ontProjectId)
}
}
// -- hub only -- //
case class Hub(shouldQuerySelf:Boolean, create:Boolean,
downstreamNodes:Iterable[DownstreamNode])
object Hub{
def apply(parsedConfig:ConfigParser):Hub = {
val shouldQuerySelf = parsedConfig.ShouldQuerySelf
val create = parsedConfig.IsHub
val downstreamNodes = parsedConfig.DownstreamNodes
Hub(shouldQuerySelf, create, downstreamNodes)
}
}
// -- adapter info -- //
case class Adapter(crcEndpointUrl:String, setSizeObfuscation:Boolean, adapterLockoutAttemptsThreshold:Int,
adapterMappingsFilename:String)
object Adapter{
def apply(parsedConfig:ConfigParser):Adapter = {
val key = "shrine.adapter."
val crcEndpointUrl = parsedConfig.shrineMap.getOrElse(key + "crcEndpoint.url","")
val setSizeObfuscation = parsedConfig.shrineMap.getOrElse(key + "setSizeObfuscation","").toLowerCase == "true"
val adapterLockoutAttemptsThreshold = parsedConfig.shrineMap.getOrElse(key + "adapterLockoutAttemptsThreshold", "0").toInt
val adapterMappingsFileName = parsedConfig.shrineMap.getOrElse(key + "adapterMappingsFileName","")
Adapter(crcEndpointUrl, setSizeObfuscation, adapterLockoutAttemptsThreshold, adapterMappingsFileName)
}
}
case class Steward(qepUserName:String, stewardBaseUrl:String)
object Steward {
def apply (parsedConfig:ConfigParser):Steward = {
val key = "shrine.queryEntryPoint.shrineSteward."
val qepUserName = parsedConfig.shrineMap.getOrElse(key + "qepUserName","")
val stewardBaseUrl = parsedConfig.shrineMap.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:ConfigParser):Audit = {
val key = "shrine.queryEntryPoint.audit."
val createTablesOnStart = parsedConfig.shrineMap.getOrElse(key + "database.createTablesOnStart","") == "true"
val dataSourceFrom = parsedConfig.shrineMap.getOrElse(key + "database.dataSourceFrom","")
val jndiDataSourceName = parsedConfig.shrineMap.getOrElse(key + "database.jndiDataSourceName","")
val slickProfileClassName = parsedConfig.shrineMap.getOrElse(key + "database.slickProfileClassName","")
val collectQepAudit = parsedConfig.shrineMap.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:ConfigParser):QEP = QEP(
maxQueryWaitTimeMinutes = parsedConfig.shrineMap.getOrElse(key + "maxQueryWaitTime.minutes", "0").toInt,
create = parsedConfig.shrineMap.getOrElse(key + "create","") == "true",
attachSigningCert = parsedConfig.shrineMap.getOrElse(key + "attachSigningCert","") == "true",
authorizationType = parsedConfig.shrineMap.getOrElse(key + "authorizationType",""),
includeAggregateResults = parsedConfig.shrineMap.getOrElse(key + "includeAggregateResults","") == "true",
authenticationType = parsedConfig.shrineMap.getOrElse(key + "authenticationType", ""),
audit = Audit(parsedConfig),
shrineSteward = Steward(parsedConfig),
broadcasterServiceEndpointUrl = parsedConfig.shrineMap.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)
}
}

Event Timeline