-
Notifications
You must be signed in to change notification settings - Fork 359
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
…ing private workflows (#7392)
- Loading branch information
1 parent
2f8c46d
commit d7def8d
Showing
11 changed files
with
330 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
10 changes: 10 additions & 0 deletions
10
services/src/main/scala/cromwell/services/auth/ecm/EcmConfig.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package cromwell.services.auth.ecm | ||
|
||
import com.typesafe.config.Config | ||
import net.ceedubs.ficus.Ficus._ | ||
|
||
final case class EcmConfig(baseUrl: Option[String]) | ||
|
||
object EcmConfig { | ||
def apply(config: Config): EcmConfig = EcmConfig(config.as[Option[String]]("ecm.base-url")) | ||
} |
58 changes: 58 additions & 0 deletions
58
services/src/main/scala/cromwell/services/auth/ecm/EcmService.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package cromwell.services.auth.ecm | ||
|
||
import akka.actor.ActorSystem | ||
import akka.http.scaladsl.Http | ||
import akka.http.scaladsl.model._ | ||
import akka.http.scaladsl.model.headers.RawHeader | ||
import akka.util.ByteString | ||
import cromwell.services.auth.GithubAuthVending.{GithubToken, TerraToken} | ||
import spray.json._ | ||
|
||
import scala.concurrent.{ExecutionContext, Future} | ||
import scala.util.{Success, Try} | ||
|
||
class EcmService(baseEcmUrl: String) { | ||
private val getGithubAccessTokenApiPath = "api/oauth/v1/github/access-token" | ||
|
||
/* | ||
ECM does generally return standard JSON error response, but for 401 status code it seems some other layer in | ||
between (like the apache proxies, etc) returns HTML pages. This helper method returns custom error message for 401 | ||
status code as it contains HTML tags. For all other status code, the response format is generally of ErrorReport | ||
schema and this method tries to extract the actual message from the JSON object and return it. In case it fails | ||
to parse JSON, it returns the original error response body. | ||
ErrorReport schema: {"message":"<actual_error_msg>", "statusCode":<code>} | ||
*/ | ||
def extractErrorMessage(errorCode: StatusCode, responseBodyAsStr: String): String = | ||
errorCode match { | ||
case StatusCodes.Unauthorized => "Invalid or missing authentication credentials." | ||
case _ => | ||
Try(responseBodyAsStr.parseJson) match { | ||
case Success(JsObject(fields)) => | ||
fields.get("message").map(_.toString().replaceAll("\"", "")).getOrElse(responseBodyAsStr) | ||
case _ => responseBodyAsStr | ||
} | ||
} | ||
|
||
def getGithubAccessToken( | ||
userToken: TerraToken | ||
)(implicit actorSystem: ActorSystem, ec: ExecutionContext): Future[GithubToken] = { | ||
|
||
def responseEntityToFutureStr(responseEntity: ResponseEntity): Future[String] = | ||
responseEntity.dataBytes.runFold(ByteString(""))(_ ++ _).map(_.utf8String) | ||
|
||
val headers: HttpHeader = RawHeader("Authorization", s"Bearer ${userToken.value}") | ||
val httpRequest = | ||
HttpRequest(method = HttpMethods.GET, uri = s"$baseEcmUrl/$getGithubAccessTokenApiPath").withHeaders(headers) | ||
|
||
Http() | ||
.singleRequest(httpRequest) | ||
.flatMap((response: HttpResponse) => | ||
if (response.status.isFailure()) { | ||
responseEntityToFutureStr(response.entity) flatMap { errorBody => | ||
val errorMessage = extractErrorMessage(response.status, errorBody) | ||
Future.failed(new RuntimeException(s"HTTP ${response.status.value}. $errorMessage")) | ||
} | ||
} else responseEntityToFutureStr(response.entity).map(GithubToken) | ||
) | ||
} | ||
} |
45 changes: 38 additions & 7 deletions
45
services/src/main/scala/cromwell/services/auth/impl/GithubAuthVendingActor.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
services/src/test/scala/cromwell/services/auth/ecm/EcmConfigSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package cromwell.services.auth.ecm | ||
|
||
import com.typesafe.config.ConfigFactory | ||
import org.scalatest.flatspec.AnyFlatSpec | ||
import org.scalatest.matchers.should.Matchers | ||
|
||
class EcmConfigSpec extends AnyFlatSpec with Matchers { | ||
|
||
it should "parse ECM base url when present" in { | ||
val config = ConfigFactory.parseString(s""" | ||
|enabled = true | ||
|auth.azure = true | ||
|ecm.base-url = "https://mock-ecm-url.org" | ||
""".stripMargin) | ||
|
||
val actualEcmConfig = EcmConfig(config) | ||
|
||
actualEcmConfig.baseUrl shouldBe defined | ||
actualEcmConfig.baseUrl.get shouldBe "https://mock-ecm-url.org" | ||
} | ||
|
||
it should "return None when ECM base url is absent" in { | ||
val config = ConfigFactory.parseString(s""" | ||
|enabled = true | ||
|auth.azure = true | ||
""".stripMargin) | ||
|
||
EcmConfig(config).baseUrl shouldBe None | ||
} | ||
} |
57 changes: 57 additions & 0 deletions
57
services/src/test/scala/cromwell/services/auth/ecm/EcmServiceSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package cromwell.services.auth.ecm | ||
|
||
import akka.http.scaladsl.model.StatusCodes | ||
import org.scalatest.flatspec.AnyFlatSpec | ||
import org.scalatest.matchers.should.Matchers | ||
import org.scalatest.prop.TableDrivenPropertyChecks | ||
|
||
class EcmServiceSpec extends AnyFlatSpec with Matchers with TableDrivenPropertyChecks { | ||
|
||
private val ecmService = new EcmService("https://mock-ecm-url.org") | ||
|
||
private val ecm400ErrorMsg = "No enum constant bio.terra.externalcreds.generated.model.Provider.MyOwnProvider" | ||
private val ecm404ErrorMsg = | ||
"No linked account found for user ID: 123 and provider: github. Please go to the Terra Profile page External Identities tab to link your account for this provider" | ||
|
||
private val testCases = Table( | ||
("test name", "response status code", "response body string", "expected error message"), | ||
("return custom 401 error when status code is 401", | ||
StatusCodes.Unauthorized, | ||
"<h2>could be anything</h2>", | ||
"Invalid or missing authentication credentials." | ||
), | ||
("extract message from valid ErrorReport JSON if status code is 400", | ||
StatusCodes.BadRequest, | ||
s"""{ "message" : "$ecm400ErrorMsg", "statusCode" : 400}""", | ||
ecm400ErrorMsg | ||
), | ||
("extract message from valid ErrorReport JSON if status code is 404", | ||
StatusCodes.NotFound, | ||
s"""{ "message" : "$ecm404ErrorMsg", "statusCode" : 404}""", | ||
ecm404ErrorMsg | ||
), | ||
("extract message from valid ErrorReport JSON if status code is 500", | ||
StatusCodes.InternalServerError, | ||
"""{ "message" : "Internal error", "statusCode" : 500}""", | ||
"Internal error" | ||
), | ||
("return response error body if it fails to parse JSON", | ||
StatusCodes.InternalServerError, | ||
"Response error - not a JSON", | ||
"Response error - not a JSON" | ||
), | ||
("return response error body if JSON doesn't contain 'message' key", | ||
StatusCodes.BadRequest, | ||
"""{"non-message-key" : "error message"}""", | ||
"""{"non-message-key" : "error message"}""" | ||
) | ||
) | ||
|
||
behavior of "extractErrorMessage in EcmService" | ||
|
||
forAll(testCases) { (testName, statusCode, responseBodyAsStr, expectedErrorMsg) => | ||
it should testName in { | ||
assert(ecmService.extractErrorMessage(statusCode, responseBodyAsStr) == expectedErrorMsg) | ||
} | ||
} | ||
} |
Oops, something went wrong.