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 36f83c3f9..df3ad7b26 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,527 +1,554 @@
 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.{ProblemDigest, Problems}
+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
 import org.json4s.native.JsonMethods.{parse => json4sParse}
 import org.json4s.{DefaultFormats, Formats}
 import shapeless.HNil
 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 ~ 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
   }
 
   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("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 = 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._
 
-      val urlToParse: String = KeyStoreInfo.keyStoreDescriptor.trustModel match {
-        case SingleHubModel(false) => ConfigSource.config.getString("shrine.queryEntryPoint.broadcasterServiceEndpoint.url")
-        case _ => ConfigSource.config.getObject("shrine.hub.downstreamNodes").values.head.unwrapped.toString
+      // 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)))
+            }
       }
+    }
+  }
 
-      val remoteDashboardPort = urlToParse.split(':')(2).split('/')(0) // TODO: Do ports vary between sites?
-      val remoteDashboardProtocol = urlToParse.split("://")(0)
-      val remoteDashboardPathPrefix = "shrine-dashboard/fromDashboard" // I don't think this needs to be configurable
+  case class MalformedURLProblem(malformattedURLException: Throwable, malformattedURL: String) extends AbstractProblem(ProblemSources.Commons) {
+    override val throwable = Some(malformattedURLException)
 
-      val baseUrl = s"$remoteDashboardProtocol://$dnsName:$remoteDashboardPort/$remoteDashboardPathPrefix"
+    override def summary: String = s"Encountered a malformatted url `$malformattedURL` while parsing urls from downstream nodes"
 
-      forwardUnmatchedPath(baseUrl,Some(ShrineJwtAuthenticator.createOAuthCredentials(user, dnsName)))
-    }
+    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("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 584f9eecb..c10f1114d 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,434 +1,434 @@
 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 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 {
+  "DashboardService" should  "return an OK and the right version string for an admin/happy/all?extra=true test" in {
 
-    Get(s"/admin/happy/all") ~>
+    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 find a bogus web service to talk to" in {
-
+  "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(NotFound)(status)
+      assertResult(StatusCodes.Forbidden)(status)
     }
   }
 
 }