diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/FireCloudApiService.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/FireCloudApiService.scala index b553708fb..a2d3afc36 100644 --- a/src/main/scala/org/broadinstitute/dsde/firecloud/FireCloudApiService.scala +++ b/src/main/scala/org/broadinstitute/dsde/firecloud/FireCloudApiService.scala @@ -71,20 +71,18 @@ trait FireCloudApiService with RegisterApiService with WorkspaceApiService with WorkspaceV2ApiService - with NotificationsApiService with MethodConfigurationApiService - with BillingApiService with SubmissionApiService with StatusApiService with MethodsApiService - with Ga4ghApiService with UserApiService with ShareLogApiService with ManagedGroupApiService with CromIamApiService with HealthApiService with StaticNotebooksApiService - with PerimeterApiService { + with PerimeterApiService + with PassthroughApiService { override lazy val log = LoggerFactory.getLogger(getClass) @@ -184,7 +182,6 @@ trait FireCloudApiService methodConfigurationRoutes ~ submissionServiceRoutes ~ nihRoutes ~ - billingServiceRoutes ~ shareLogServiceRoutes ~ staticNotebooksRoutes ~ perimeterServiceRoutes @@ -217,14 +214,14 @@ trait FireCloudApiService managedGroupServiceRoutes ~ workspaceRoutes ~ workspaceV2Routes ~ - notificationsRoutes ~ statusRoutes ~ - ga4ghRoutes ~ pathPrefix("api") { apiRoutes } ~ // insecure cookie-authed routes - cookieAuthedRoutes + cookieAuthedRoutes ~ + // wildcard passthrough routes. These must be last to allow other routes to override them. + passthroughRoutes } } diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/BillingApiService.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/BillingApiService.scala deleted file mode 100644 index 548947d04..000000000 --- a/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/BillingApiService.scala +++ /dev/null @@ -1,19 +0,0 @@ -package org.broadinstitute.dsde.firecloud.webservice - -import akka.http.scaladsl.model.HttpMethods._ -import akka.http.scaladsl.model.Uri -import akka.http.scaladsl.server.Route -import org.broadinstitute.dsde.firecloud.FireCloudConfig -import org.broadinstitute.dsde.firecloud.service.FireCloudDirectives -import org.broadinstitute.dsde.firecloud.utils.StreamingPassthrough - -trait BillingApiService extends FireCloudDirectives with StreamingPassthrough { - private val userBillingUrl = FireCloudConfig.Rawls.authUrl + "/user/billing" - - val billingServiceRoutes: Route = - pathPrefix("billing") { - // all paths under /api/billing pass through to the same path in Rawls - streamingPassthrough(Uri.Path("/api/billing") -> Uri(FireCloudConfig.Rawls.authUrl + "/billing")) - } - -} diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/Ga4ghApiService.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/Ga4ghApiService.scala deleted file mode 100644 index bd0324ce2..000000000 --- a/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/Ga4ghApiService.scala +++ /dev/null @@ -1,74 +0,0 @@ -package org.broadinstitute.dsde.firecloud.webservice - -import akka.http.scaladsl.model.Uri.Query -import akka.http.scaladsl.model.{HttpMethods, Uri} -import akka.http.scaladsl.server.Route -import org.broadinstitute.dsde.firecloud.FireCloudConfig -import org.broadinstitute.dsde.firecloud.service.FireCloudDirectives - -import scala.concurrent.ExecutionContext - -trait Ga4ghApiService extends FireCloudDirectives { - - implicit val executionContext: ExecutionContext - - private lazy val agoraGA4GH = s"${FireCloudConfig.Agora.baseUrl}/ga4gh/v1" - - val ga4ghRoutes: Route = - pathPrefix("ga4gh") { - pathPrefix("v1") { - get { - path("metadata") { - val targetUri = Uri(s"$agoraGA4GH/metadata") - passthrough(targetUri, HttpMethods.GET) - } ~ - path("tool-classes") { - val targetUri = Uri(s"$agoraGA4GH/tool-classes") - passthrough(targetUri, HttpMethods.GET) - } ~ - path("tools") { - parameterSeq { params => - val targetUri = Uri(s"$agoraGA4GH/tools") - val uri = if (params.isEmpty) { - targetUri - } else { - targetUri.withQuery(Query(params.toMap)) - } - passthrough(uri, HttpMethods.GET) - } - } ~ - path("tools" / Segment) { id => - val targetUri = Uri(s"$agoraGA4GH/tools/$id") - passthrough(targetUri, HttpMethods.GET) - } ~ - path("tools" / Segment / "versions") { id => - val targetUri = Uri(s"$agoraGA4GH/tools/$id/versions") - passthrough(targetUri, HttpMethods.GET) - } ~ - path("tools" / Segment / "versions" / Segment / "dockerfile") { (id, versionId) => - val targetUri = Uri(s"$agoraGA4GH/tools/$id/versions/$versionId/dockerfile") - passthrough(targetUri, HttpMethods.GET) - } ~ - path("tools" / Segment / "versions" / Segment) { (id, versionId) => - val targetUri = Uri(s"$agoraGA4GH/tools/$id/versions/$versionId") - passthrough(targetUri, HttpMethods.GET) - } ~ - path("tools" / Segment / "versions" / Segment / Segment / "descriptor") { (id, versionId, descriptorType) => - val targetUri = Uri(s"$agoraGA4GH/tools/$id/versions/$versionId/$descriptorType/descriptor") - passthrough(targetUri, HttpMethods.GET) - } ~ - path("tools" / Segment / "versions" / Segment / Segment / "descriptor" / Segment) { - (id, versionId, descriptorType, relativePath) => - val targetUri = - Uri(s"$agoraGA4GH/tools/$id/versions/$versionId/$descriptorType/descriptor/$relativePath") - passthrough(targetUri, HttpMethods.GET) - } ~ - path("tools" / Segment / "versions" / Segment / Segment / "tests") { (id, versionId, descriptorType) => - val targetUri = Uri(s"$agoraGA4GH/tools/$id/versions/$versionId/$descriptorType/tests") - passthrough(targetUri, HttpMethods.GET) - } - } - } - } - -} diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/NotificationsApiService.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/NotificationsApiService.scala deleted file mode 100644 index 014af21f3..000000000 --- a/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/NotificationsApiService.scala +++ /dev/null @@ -1,26 +0,0 @@ -package org.broadinstitute.dsde.firecloud.webservice - -import akka.http.scaladsl.model.HttpMethods.GET -import akka.http.scaladsl.server.Route -import org.broadinstitute.dsde.firecloud.FireCloudConfig -import org.broadinstitute.dsde.firecloud.service.FireCloudDirectives -import org.broadinstitute.dsde.firecloud.utils.StandardUserInfoDirectives - -trait NotificationsApiService extends FireCloudDirectives with StandardUserInfoDirectives { - final private val ApiPrefix = "api/notifications" - final private val General = "general" - final private val Workspace = "workspace" - final private val RawlsNotifications = FireCloudConfig.Rawls.notificationsUrl - - final val notificationsRoutes: Route = - get { - pathPrefix(separateOnSlashes(ApiPrefix)) { - path(General) { - passthrough(encodeUri(s"$RawlsNotifications/$General"), GET) - } ~ - path(Workspace / Segment / Segment) { (namespace, name) => - passthrough(encodeUri(s"$RawlsNotifications/$Workspace/$namespace/$name"), GET) - } - } - } -} diff --git a/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/PassthroughApiService.scala b/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/PassthroughApiService.scala new file mode 100644 index 000000000..069f3d013 --- /dev/null +++ b/src/main/scala/org/broadinstitute/dsde/firecloud/webservice/PassthroughApiService.scala @@ -0,0 +1,18 @@ +package org.broadinstitute.dsde.firecloud.webservice + +import akka.http.scaladsl.server.{Directives, Route} +import org.broadinstitute.dsde.firecloud.FireCloudConfig +import org.broadinstitute.dsde.firecloud.utils.StreamingPassthrough + +trait PassthroughApiService extends Directives with StreamingPassthrough { + + private lazy val agora = FireCloudConfig.Agora.baseUrl + private lazy val rawls = FireCloudConfig.Rawls.baseUrl + + val passthroughRoutes: Route = concat( + pathPrefix("ga4gh")(streamingPassthrough(s"$agora/ga4gh")), + pathPrefix("api" / "billing")(streamingPassthrough(s"$rawls/api/billing")), + pathPrefix("api" / "notifications")(streamingPassthrough(s"$rawls/api/notifications")) + ) + +} diff --git a/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/BillingApiServiceSpec.scala b/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/BillingApiServiceSpec.scala deleted file mode 100644 index 40da1d358..000000000 --- a/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/BillingApiServiceSpec.scala +++ /dev/null @@ -1,94 +0,0 @@ -package org.broadinstitute.dsde.firecloud.webservice - -import akka.http.scaladsl.model.HttpMethods._ -import akka.http.scaladsl.model.StatusCodes._ -import akka.http.scaladsl.server.Route.{seal => sealRoute} -import org.broadinstitute.dsde.firecloud.FireCloudConfig -import org.broadinstitute.dsde.firecloud.mock.MockUtils -import org.broadinstitute.dsde.firecloud.service.BaseServiceSpec -import org.mockserver.integration.ClientAndServer -import org.mockserver.integration.ClientAndServer._ -import org.mockserver.model.HttpRequest._ -import org.mockserver.model.HttpResponse - -import scala.concurrent.ExecutionContext - -final class BillingApiServiceSpec extends BaseServiceSpec with BillingApiService { - - override val executionContext: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - - var workspaceServer: ClientAndServer = _ - - override def beforeAll(): Unit = { - val billingPath = FireCloudConfig.Rawls.authPrefix + "/billing" - - workspaceServer = startClientAndServer(MockUtils.workspaceServerPort) - - workspaceServer - .when( - request() - .withMethod(POST.name) - .withPath(billingPath) - ) - .respond( - HttpResponse - .response() - .withHeaders(MockUtils.header) - .withStatusCode(Created.intValue) - ) - - workspaceServer - .when( - request() - .withMethod(GET.name) - .withPath(billingPath + "/project1/members") - ) - .respond( - HttpResponse - .response() - .withHeaders(MockUtils.header) - .withStatusCode(OK.intValue) - ) - - List(PUT, DELETE).foreach { method => - workspaceServer - .when( - request() - .withMethod(method.name) - .withPath(billingPath + "/project2/user/foo@bar.com") - ) - .respond( - HttpResponse - .response() - .withHeaders(MockUtils.header) - .withStatusCode(OK.intValue) - ) - } - } - - override def afterAll(): Unit = - workspaceServer.stop() - - // streamingPassthrough directive needs to see the routes under "/api", which is how FireCloudApiService starts them - val testableRoutes = pathPrefix("api")(billingServiceRoutes) - - "BillingApiService" - { - "list project members" in { - Get("/api/billing/project1/members") ~> dummyAuthHeaders ~> sealRoute(testableRoutes) ~> check { - status should be(OK) - } - } - - "add user" in { - Put("/api/billing/project2/user/foo@bar.com") ~> dummyAuthHeaders ~> sealRoute(testableRoutes) ~> check { - status should be(OK) - } - } - - "remove user" in { - Delete("/api/billing/project2/user/foo@bar.com") ~> dummyAuthHeaders ~> sealRoute(testableRoutes) ~> check { - status should be(OK) - } - } - } -} diff --git a/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/Ga4ghApiServiceSpec.scala b/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/Ga4ghApiServiceSpec.scala deleted file mode 100644 index a4a5ae67c..000000000 --- a/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/Ga4ghApiServiceSpec.scala +++ /dev/null @@ -1,76 +0,0 @@ -package org.broadinstitute.dsde.firecloud.webservice - -import akka.actor.ActorSystem -import org.broadinstitute.dsde.firecloud.mock.MockUtils -import org.broadinstitute.dsde.firecloud.service.BaseServiceSpec -import org.mockserver.integration.ClientAndServer -import org.mockserver.integration.ClientAndServer.startClientAndServer -import org.mockserver.model.HttpRequest.request -import org.scalatest.BeforeAndAfterEach -import akka.http.scaladsl.model.HttpMethods -import akka.http.scaladsl.model.StatusCodes._ - -import scala.concurrent.ExecutionContext - -class Ga4ghApiServiceSpec extends BaseServiceSpec with Ga4ghApiService with BeforeAndAfterEach { - - def actorRefFactory: ActorSystem = system - - override val executionContext: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - - var toolRegistryServer: ClientAndServer = _ - val toolPaths = List( - "/ga4gh/v1/metadata", - "/ga4gh/v1/tool-classes", - "/ga4gh/v1/tools", - "/ga4gh/v1/tools/namespace:name", - "/ga4gh/v1/tools/namespace:name/versions", - "/ga4gh/v1/tools/namespace:name/versions/1", - "/ga4gh/v1/tools/namespace:name/versions/1/WDL/descriptor", - // The following paths are currently unimplemented in Agora, but handled. - "/ga4gh/v1/tools/namespace:name/versions/1/dockerfile", - "/ga4gh/v1/tools/namespace:name/versions/1/WDL/descriptor/1", - "/ga4gh/v1/tools/namespace:name/versions/1/WDL/tests" - ) - - override def beforeAll(): Unit = { - toolRegistryServer = startClientAndServer(MockUtils.methodsServerPort) - toolPaths.map { path => - toolRegistryServer - .when(request().withMethod(HttpMethods.GET.name).withPath(path)) - .respond( - org.mockserver.model.HttpResponse - .response() - .withStatusCode(OK.intValue) - ) - } - } - - override def afterAll(): Unit = - toolRegistryServer.stop() - - "GA4GH API service" - { - "Tool Registry" - { - "passthrough APIs" - { - "should reject all but GET" in { - List(HttpMethods.POST, HttpMethods.PUT, HttpMethods.PATCH, HttpMethods.DELETE, HttpMethods.HEAD) foreach { - method => - toolPaths.map { path => - new RequestBuilder(method)(path) ~> ga4ghRoutes ~> check { - assert(!handled) - } - } - } - } - "should accept GET" in { - toolPaths.map { path => - Get(path) ~> ga4ghRoutes ~> check { - assert(handled) - } - } - } - } - } - } - -} diff --git a/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/NotificationsApiServiceNegativeSpec.scala b/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/NotificationsApiServiceNegativeSpec.scala deleted file mode 100644 index 79690e6f1..000000000 --- a/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/NotificationsApiServiceNegativeSpec.scala +++ /dev/null @@ -1,42 +0,0 @@ -package org.broadinstitute.dsde.firecloud.webservice - -import akka.http.scaladsl.model.HttpMethods.GET -import org.broadinstitute.dsde.firecloud.mock.MockWorkspaceServer -import org.broadinstitute.dsde.firecloud.service.ServiceSpec - -import scala.concurrent.ExecutionContext - -/** - * We don't create a mock server so we can differentiate between methods that get passed through (and result in - * InternalServerError) and those that aren't passed through at the first place (i.e. not 'handled') - */ -final class NotificationsApiServiceNegativeSpec extends ServiceSpec with NotificationsApiService { - - override val executionContext: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - - "NotificationsApiService" - { - val namespace = MockWorkspaceServer.mockValidWorkspace.namespace - val name = MockWorkspaceServer.mockValidWorkspace.name - val workspaceNotificationUri = s"/api/notifications/workspace/$namespace/$name" - - val generalNotificationUri = "/api/notifications/general" - - // N.B. this file used to contain positive passthrough tests that checked to see if the request was handled - // by the route. However, those tests are unstable without a mockserver behind the passthrough or other way - // of actually handling the test; Akka shuts down its connection pool when it sees that requests are not connecting - // to the target. I have removed these positive tests, since we have coverage for them anyway in NotificationsApisServiceSpec. - - "non-GET requests should not be passed through for general notifications" in { - allHttpMethodsExcept(GET) foreach { method => - checkIfPassedThrough(notificationsRoutes, method, generalNotificationUri, toBeHandled = false) - } - } - - "non-GET requests should not be passed through for workspace notifications" in { - allHttpMethodsExcept(GET) foreach { method => - checkIfPassedThrough(notificationsRoutes, method, workspaceNotificationUri, toBeHandled = false) - } - } - } - -} diff --git a/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/NotificationsApiServiceSpec.scala b/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/NotificationsApiServiceSpec.scala deleted file mode 100644 index 6d8ee4efa..000000000 --- a/src/test/scala/org/broadinstitute/dsde/firecloud/webservice/NotificationsApiServiceSpec.scala +++ /dev/null @@ -1,59 +0,0 @@ -package org.broadinstitute.dsde.firecloud.webservice - -import akka.http.scaladsl.model.HttpMethods.GET -import akka.http.scaladsl.model.StatusCodes.{MethodNotAllowed, NotFound, OK} -import akka.http.scaladsl.model.{HttpMethod, StatusCode} -import akka.http.scaladsl.server.Route.{seal => sealRoute} -import org.broadinstitute.dsde.firecloud.mock.MockWorkspaceServer -import org.broadinstitute.dsde.firecloud.service.BaseServiceSpec - -import scala.concurrent.ExecutionContext - -final class NotificationsApiServiceSpec extends BaseServiceSpec with NotificationsApiService { - - override val executionContext: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global - - override def beforeAll(): Unit = - MockWorkspaceServer.startWorkspaceServer() - - override def afterAll(): Unit = - MockWorkspaceServer.stopWorkspaceServer() - - "NotificationsApiService" - { - "get workspace notifications" in { - val namespace = MockWorkspaceServer.mockValidWorkspace.namespace - val name = MockWorkspaceServer.mockValidWorkspace.name - val workspaceNotificationUri = s"/api/notifications/workspace/$namespace/$name" - - doAssert(GET, workspaceNotificationUri, OK) - } - - "get general notifications" in { - doAssert(GET, "/api/notifications/general", OK) - } - - "non-GET methods should be rejected" in { - allHttpMethodsExcept(GET) foreach { method => - doAssert(method, "/api/notifications/general", MethodNotAllowed) - } - } - - "edge-case URIs should be rejected" in { - val edgeCaseURIs = Seq( - "/api/notifications", - "/api/notifications/somethingNotValid", - "/api/notifications/workspace/workspaceNamespace", - "/api/notifications/workspace/workspaceNamespace/workspaceName/something" - ) - - edgeCaseURIs foreach { uri => - doAssert(GET, uri, NotFound) - } - } - } - - private def doAssert(method: HttpMethod, uri: String, expectedStatus: StatusCode): Unit = - new RequestBuilder(method)(uri) ~> dummyAuthHeaders ~> sealRoute(notificationsRoutes) ~> check { - status should be(expectedStatus) - } -}