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 0933f3768..8fe709c5a 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,572 +1,575 @@ 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.{BouncyKeyStoreCollection, KeyStoreDescriptorParser, UtilHasher} import net.shrine.dashboard.httpclient.HttpClientDirectives.{forwardUnmatchedPath, requestUriThenRoute} import net.shrine.dashboard.jwtauth.ShrineJwtAuthenticator import net.shrine.i2b2.protocol.pm.User import net.shrine.log.Loggable import net.shrine.problem.{AbstractProblem, ProblemDigest, ProblemSources, Problems} import net.shrine.serialization.NodeSeqSerializer import net.shrine.source.ConfigSource import net.shrine.spray._ import net.shrine.status.protocol.{Config => StatusProtocolConfig} import net.shrine.util.{SingleHubModel, Versions} import org.json4s.native.JsonMethods.{parse => json4sParse} import org.json4s.{DefaultFormats, Formats} import shapeless.HNil -import spray.http._ +import spray.http.{HttpRequest, HttpResponse, StatusCodes, Uri} import spray.httpx.Json4sSupport import spray.routing._ import spray.routing.directives.LogEntry import scala.collection.immutable.Iterable import scala.concurrent.ExecutionContext.Implicits.global import scala.util.{Failure, Success, Try} /** * 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(ConfigSource.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 ~ versionCheck ~ makeTrouble ~ about ~ authenticatedInBrowser ~ authenticatedDashboard ~ post { // Chicken and egg problem; Can't check status of certs validation between sites if you need valid certs to exchange messages pathPrefix("status") pathPrefix("verifySignature") verifySignature } } /** 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 } lazy val versionCheck = pathPrefix("version"){ - - val currentVersion = Versions.version - val buildDate = Versions.buildDate - - val response: AppVersion = AppVersion(currentVersion, buildDate) + val response: AppVersion = AppVersion() implicit val formats = response.json4sMarshaller complete(response) } - case class AppVersion(currentVersion:String, buildDate:String) extends DefaultJsonSupport { + case class AppVersion( + currentVersion:String, + buildDate:String + ) extends DefaultJsonSupport { override def toString: String = { - "{\"currentVersion\":" + "\"" + currentVersion + "\"" + "," + "\"buildDate\":\"" + buildDate + "\"}" + s"""{"currentVersion":"$currentVersion","buildDate":"$buildDate"}""" } } + object AppVersion { + def apply(): AppVersion = AppVersion(Versions.version, Versions.buildDate) + } + 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 => info(s"Sucessfully authenticated user `$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("Something is here already") //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 = ConfigSource.config.getString("shrine.dashboard.happyBaseUrl") forwardUnmatchedPath(happyBaseUrl) } ~ pathPrefix("messWithHappyVersion") { //todo is this used? val happyBaseUrl: String = ConfigSource.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/ping" /** * Forward a request from this dashboard to a remote dashboard */ def toDashboardRoute(user:User):Route = get { pathPrefix(Segment) { dnsName => import scala.collection.JavaConversions._ // Check that it makes sense to call toDashboard KeyStoreInfo.keyStoreDescriptor.trustModel match { case SingleHubModel(false) => warn("toDashboard route called on a non-hub node, returning Forbidden") complete(StatusCodes.Forbidden) case _ => ConfigSource.config.getObject("shrine.hub.downstreamNodes") .values .map(cv => Try(new java.net.URL(cv.unwrapped().toString)) match { case Failure(exception) => MalformedURLProblem(exception, cv.unwrapped().toString) throw exception case Success(goodUrl) => goodUrl }) .find(_.getHost == dnsName) match { case None => warn(s"Could not find a downstream node matching the requested host `$dnsName`, returning NotFound") complete(StatusCodes.NotFound) case Some(downstreamUrl) => val remoteDashboardPathPrefix = downstreamUrl.getPath .replaceFirst("shrine/rest/adapter/requests", "shrine-dashboard/fromDashboard") // I don't think this needs to be configurable val port = if (downstreamUrl.getPort == -1) downstreamUrl.getDefaultPort else downstreamUrl.getPort val baseUrl = s"${downstreamUrl.getProtocol}://$dnsName:$port$remoteDashboardPathPrefix" info(s"toDashboardRoute: BaseURL: $baseUrl") forwardUnmatchedPath(baseUrl,Some(ShrineJwtAuthenticator.createOAuthCredentials(user, dnsName))) } } } } case class MalformedURLProblem(malformattedURLException: Throwable, malformattedURL: String) extends AbstractProblem(ProblemSources.Dashboard) { override val throwable = Some(malformattedURLException) override def summary: String = s"Encountered a malformatted url `$malformattedURL` while parsing urls from downstream nodes" override def description: String = description } 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 = ConfigSource.config.getString("shrine.dashboard.statusBaseUrl") // TODO: Move this over to Status API? lazy val verifySignature:Route = { formField("sha256".as[String].?) { sha256: Option[String] => val response = sha256.map(s => KeyStoreInfo.hasher.handleSig(s)) implicit val format = ShaResponse.json4sFormats response match { case None => complete(StatusCodes.BadRequest) case Some(sh@ShaResponse(ShaResponse.badFormat, _)) => complete(StatusCodes.BadRequest -> sh) case Some(sh@ShaResponse(_, false)) => complete(StatusCodes.NotFound -> sh) case Some(sh@ShaResponse(_, true)) => complete(StatusCodes.OK -> sh) } } } 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 } object KeyStoreInfo { val config = ConfigSource.config val keyStoreDescriptor = KeyStoreDescriptorParser( config.getConfig("shrine.keystore"), config.getConfigOrEmpty("shrine.hub"), config.getConfigOrEmpty("shrine.queryEntryPoint")) val certCollection = BouncyKeyStoreCollection.fromFileRecoverWithClassPath(keyStoreDescriptor) val hasher = UtilHasher(certCollection) } /** * 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.AllOrigins import spray.http.HttpHeaders.{`Access-Control-Allow-Headers`, `Access-Control-Allow-Methods`, `Access-Control-Allow-Origin`, `Access-Control-Max-Age`} import spray.http.HttpMethods.{GET, OPTIONS, POST} import spray.routing.directives.MethodDirectives.options import spray.routing.directives.RespondWithDirectives.respondWithHeaders import spray.routing.directives.RouteDirectives.complete 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 = ConfigSource.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) } } \ No newline at end of file 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 3786e5324..854a1f301 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,451 +1,447 @@ 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.config.ConfigExtensions import net.shrine.crypto.{BouncyKeyStoreCollection, KeyStoreDescriptorParser} import net.shrine.dashboard.jwtauth.ShrineJwtAuthenticator import net.shrine.i2b2.protocol.pm.User import net.shrine.protocol.Credential import net.shrine.source.ConfigSource import net.shrine.spray.ShaResponse import net.shrine.util.Versions 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?extra=true test" in { Get(s"/admin/happy/all?extra=true") ~> 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("/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("/status/verifySignature", FormData(Seq("sha256" -> "foo"))) ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(StatusCodes.BadRequest)(status) implicit val formats = ShaResponse.json4sFormats assertResult(ShaResponse(ShaResponse.badFormat, false))(parse(new String(body.data.toByteArray)).extract[ShaResponse]) } } "DashboardService" should "return a NotFound for admin/status/signature with a correctly formatted parameter that is not in the keystore" in { Post("/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(NotFound)(status) implicit val formats = ShaResponse.json4sFormats assertResult(ShaResponse("0E:5D:D1:10:68:2B:63:F4:66:E2:50:41:EA:13:AF:1A:F9:99:DB:40:6A:F7:EE:39:F2:1A:0D:51:7A:44:09:7A", false)) ( parse(new String(body.data.toByteArray)).extract[ShaResponse]) } } "DashboardService" should "return an OK for admin/status/signature with a valid sha256 hash" in { val post = Post("/status/verifySignature", FormData(Seq("sha256" -> "0E:5D:D1:10:68:2B:63:F4:66:E2:50:41:EA:13:AF:1A:F9:99:DB:40:6A:F7:EE:39:F2:1A:0D:51:7A:44:09:7A"))) post ~> addCredentials(adminCredentials) ~> route ~> check { assertResult(OK)(status) implicit val formats = ShaResponse.json4sFormats assertResult(ShaResponse("0E:5D:D1:10:68:2B:63:F4:66:E2:50:41:EA:13:AF:1A:F9:99:DB:40:6A:F7:EE:39:F2:1A:0D:51:7A:44:09:7A", true))( parse(new String(body.data.toByteArray)).extract[ShaResponse] ) } } 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 = ConfigSource.config val shrineCertCollection: BouncyKeyStoreCollection = BouncyKeyStoreCollection.fromFileRecoverWithClassPath(KeyStoreDescriptorParser( config.getConfig("shrine.keystore"), config.getConfigOrEmpty("shrine.hub"), config.getConfigOrEmpty("shrine.queryEntryPoint"))) val base64Cert = new String(TextCodec.BASE64URL.encode(shrineCertCollection.myEntry.cert.getEncoded)) val key: PrivateKey = shrineCertCollection.myEntry.privateKey.get 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 = ConfigSource.config val shrineCertCollection: BouncyKeyStoreCollection = BouncyKeyStoreCollection.fromFileRecoverWithClassPath(KeyStoreDescriptorParser( config.getConfig("shrine.keystore"), config.getConfigOrEmpty("shrine.hub"), config.getConfigOrEmpty("shrine.queryEntryPoint"))) val base64Cert = new String(TextCodec.BASE64URL.encode(shrineCertCollection.myEntry.cert.getEncoded)) val key: PrivateKey = shrineCertCollection.myEntry.privateKey.get 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 be able to make a toDashboard request" in { // Can't make a request because it's configured as a downstream node Get(s"/toDashboard/bogus.harvard.edu/ping") ~> addCredentials(adminCredentials) ~> sealRoute(route) ~> check { val string = new String(body.data.toByteArray) assertResult(StatusCodes.Forbidden)(status) } } - "DashboardService" should "be able to return a JSON object contains current app version and build date" in { + "DashboardService" should "be able to return a JSON object that contains the current app version and build date" in { Get(s"/version") ~> route ~> check { - - val currentVersion = Versions.version - val buildDate = Versions.buildDate - assertResult(OK)(status) - val response = new AppVersion(currentVersion, buildDate) + val response = AppVersion() assertResult(response.toString)(new String(body.data.toByteArray)) } } }