Skip to content

Commit

Permalink
fix: CQDG-793 update for cavatica
Browse files Browse the repository at this point in the history
  • Loading branch information
adipaul1981 committed Jul 3, 2024
1 parent f780a35 commit 4482478
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 77 deletions.
3 changes: 2 additions & 1 deletion src/main/scala/bio/ferlab/ferload/Config.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ case class DrsConfig(
selfHost: String,
organizationName: String,
organizationUrl: String,
accessId: String,
description: Option[String] = None,
contactUrl: Option[String] = None,
documentationUrl: Option[String] = None,
Expand All @@ -50,11 +51,11 @@ object DrsConfig {
sys.env("DRS_SELF_HOST"),
sys.env("DRS_ORGANIZATION_NAME"),
sys.env("DRS_ORGANIZATION_URL"),
sys.env("DRS_ACCESS_ID"),
sys.env.get("DRS_DESCRIPTION"),
sys.env.get("DRS_CONTACT_URL"),
sys.env.get("DRS_DOCUMENTATION_URL"),
sys.env.get("DRS_ENVIRONMENT"),

)
}
}
Expand Down
35 changes: 29 additions & 6 deletions src/main/scala/bio/ferlab/ferload/endpoints/DrsEndpoints.scala
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ import sttp.tapir.server.ServerEndpoint

object DrsEndpoints:
val baseEndpoint: Endpoint[Unit, Unit, Unit, Unit, Any] = endpoint
.prependSecurityIn("ga4gh")
.prependSecurityIn("drs")
.prependSecurityIn("v1")
.prependSecurityIn("drs")
.prependSecurityIn("ga4gh")

private val service = baseEndpoint.get
.in("service-info")
Expand Down Expand Up @@ -59,6 +59,17 @@ object DrsEndpoints:
.get
.out(jsonBody[DrsObject])


private def getAccessMethod(authorizationService: AuthorizationService) =
objectEnpoint
.securityIn(auth.bearer[String]())
.securityIn(path[String].name("object_id"))
.securityIn("access" / path[String].name("access_id"))
.errorOut(statusCode.and(jsonBody[ErrorResponse]))
.serverSecurityLogic((token, objectId, accessId) => authorizationService.authLogic(token, Seq(objectId), Some(accessId)))
.get
.out(jsonBody[AccessURL])

private val createObject: Endpoint[Unit, (String, CreateDrsObject), (StatusCode, ErrorResponse), StatusCode, Any] =
baseEndpoint
.in("object")
Expand All @@ -78,14 +89,25 @@ object DrsEndpoints:
}
}

private def getObjectServer(config: Config, authorizationService: AuthorizationService, resourceService: ResourceService, s3Service: S3Service) = getObject(authorizationService).serverLogicSuccess { user =>
private def getObjectServer(config: Config, authorizationService: AuthorizationService, resourceService: ResourceService) = getObject(authorizationService).serverLogicSuccess { (user, _) =>
_ =>
for {
resource <- resourceService.getResourceById(user.permissions.head.rsid)
// For now, we only have a unique accessId (all resources are in CEPH S3)
} yield DrsObject.build(resource, config.drsConfig.accessId, config.drsConfig.selfHost)
}


private def getAccessMethodServer(authorizationService: AuthorizationService, resourceService: ResourceService, s3Service: S3Service) = getAccessMethod(authorizationService).serverLogicSuccess { (user, accessId) =>
_ =>
//fetch according to accessId, it is unique for now
for {
resource <- resourceService.getResourceById(user.permissions.head.resource_id)
resource <- resourceService.getResourceById(user.permissions.head.rsid)
bucketAndPath <- IO.fromTry(S3Service.parseS3Urls(resource.uris))
(bucket, path) = bucketAndPath
url = s3Service.presignedUrl(bucket, path)
} yield DrsObject.build(resource, url, config.drsConfig.selfHost)
} yield
AccessURL.build(url)
}

private def createObjectServer(config: Config, resourceService: ResourceService) = createObject.serverLogicSuccess { (token, createDrsObject) =>
Expand All @@ -102,6 +124,7 @@ object DrsEndpoints:
def all(config: Config, authorizationService: AuthorizationService, resourceService: ResourceService, s3Service: S3Service): Seq[ServerEndpoint[Any, IO]] = Seq(
serviceServer(config.drsConfig),
objectInfoServer(config, resourceService),
getObjectServer(config, authorizationService, resourceService, s3Service),
getObjectServer(config, authorizationService, resourceService),
getAccessMethodServer(authorizationService, resourceService, s3Service),
createObjectServer(config, resourceService)
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,21 @@ import sttp.tapir.server.*
object LegacyObjectEndpoints:


private def securedGlobalEndpoint(authorizationService: AuthorizationService, resourceGlobalName: String): PartialServerEndpoint[String, User, Unit, (StatusCode, ErrorResponse), Unit, Any, IO] =
private def securedGlobalEndpoint(authorizationService: AuthorizationService, resourceGlobalName: String): PartialServerEndpoint[String, (User, Option[String]), Unit, (StatusCode, ErrorResponse), Unit, Any, IO] =
endpoint
.securityIn(auth.bearer[String]())
.errorOut(statusCode.and(jsonBody[ErrorResponse]))
.serverSecurityLogic(token => authorizationService.authLogic(token, Seq(resourceGlobalName)))

private def objectByPath(authorizationService: AuthorizationService, resourceGlobalName: String): PartialServerEndpoint[String, User, List[String], (StatusCode, ErrorResponse), ObjectUrl, Any, IO] =
private def objectByPath(authorizationService: AuthorizationService, resourceGlobalName: String): PartialServerEndpoint[String, (User, Option[String]), List[String], (StatusCode, ErrorResponse), ObjectUrl, Any, IO] =
securedGlobalEndpoint(authorizationService, resourceGlobalName)
.get
.description("Retrieve an object by its path and return an url to download it")
.deprecated()
.in(paths.description("Path of the object to retrieve"))
.out(jsonBody[ObjectUrl])

private def objectsByPaths(authorizationService: AuthorizationService, resourceGlobalName: String): PartialServerEndpoint[String, User, String, (StatusCode, ErrorResponse), Map[String, String], Any, IO] =
private def objectsByPaths(authorizationService: AuthorizationService, resourceGlobalName: String): PartialServerEndpoint[String, (User, Option[String]), String, (StatusCode, ErrorResponse), Map[String, String], Any, IO] =
securedGlobalEndpoint(authorizationService, resourceGlobalName)
.description("Retrieve a list of objects by their paths and return a list of download URLs for each object")
.deprecated()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ object ObjectsEndpoints:

private val byIdEndpoint = baseEndpoint.securityIn("objects")

private def singleObject(authorizationService: AuthorizationService): PartialServerEndpoint[(String, String), User, Unit, (StatusCode, ErrorResponse), ObjectUrl, Any, IO] = byIdEndpoint
private def singleObject(authorizationService: AuthorizationService): PartialServerEndpoint[(String, String), (User, Option[String]), Unit, (StatusCode, ErrorResponse), ObjectUrl, Any, IO] = byIdEndpoint
.get
.securityIn(path[String].name("object_id"))
.serverSecurityLogic((token, objectId) => authorizationService.authLogic(token, Seq(objectId)))
.description("Retrieve an object by its id and return an url to download it")
.out(jsonBody[ObjectUrl])

private def listObjects(authorizationService: AuthorizationService): PartialServerEndpoint[(String, String), User, Unit, (StatusCode, ErrorResponse), Map[String, String], Any, IO] = byIdEndpoint
private def listObjects(authorizationService: AuthorizationService): PartialServerEndpoint[(String, String), (User, Option[String]), Unit, (StatusCode, ErrorResponse), Map[String, String], Any, IO] = byIdEndpoint
.post
.securityIn("list")
.securityIn(stringBody.description("List of ids of objects to retrieve").example("FI1\nFI2"))
Expand All @@ -39,10 +39,10 @@ object ObjectsEndpoints:


def singleObjectServer(authorizationService: AuthorizationService, resourceService: ResourceService, s3Service: S3Service): ServerEndpoint[Any, IO] =
singleObject(authorizationService).serverLogicSuccess { user =>
singleObject(authorizationService).serverLogicSuccess { (user, _) =>
_ =>
for {
resource <- resourceService.getResourceById(user.permissions.head.resource_id)
resource <- resourceService.getResourceById(user.permissions.head.rsid)
bucketAndPath <- IO.fromTry(S3Service.parseS3Urls(resource.uris))
(bucket, path) = bucketAndPath
url = s3Service.presignedUrl(bucket, path)
Expand All @@ -52,9 +52,9 @@ object ObjectsEndpoints:


def listObjectsServer(authorizationService: AuthorizationService, resourceService: ResourceService, s3Service: S3Service): ServerEndpoint[Any, IO] =
listObjects(authorizationService).serverLogicSuccess { user =>
listObjects(authorizationService).serverLogicSuccess { (user, _) =>
_ =>
val resourcesIO: IO[List[ReadResource]] = user.permissions.toList.traverse(p => resourceService.getResourceById(p.resource_id))
val resourcesIO: IO[List[ReadResource]] = user.permissions.toList.traverse(p => resourceService.getResourceById(p.rsid))
resourcesIO.map { resources =>
val urls: Seq[(String, (String, String))] = resources.flatMap(r => S3Service.parseS3Urls(r.uris).toOption.map(r.name -> _))
val m: Map[String, String] = urls.map { case (name, (bucket, path)) => name -> s3Service.presignedUrl(bucket, path) }.toMap
Expand All @@ -70,13 +70,13 @@ object ObjectsEndpoints:
)

object ByPath:
private def byPathEndpoint(authorizationService: AuthorizationService, resourceGlobalName: String): PartialServerEndpoint[String, User, Unit, (StatusCode, ErrorResponse), Unit, Any, IO] =
private def byPathEndpoint(authorizationService: AuthorizationService, resourceGlobalName: String): PartialServerEndpoint[String, (User, Option[String]), Unit, (StatusCode, ErrorResponse), Unit, Any, IO] =
baseEndpoint
.securityIn("objects")
.securityIn("bypath")
.serverSecurityLogic(token => authorizationService.authLogic(token, Seq(resourceGlobalName)))

private def singleObject(authorizationService: AuthorizationService, resourceGlobalName: String): PartialServerEndpoint[String, User, String, (StatusCode, ErrorResponse), ObjectUrl, Any, IO] =
private def singleObject(authorizationService: AuthorizationService, resourceGlobalName: String): PartialServerEndpoint[String, (User, Option[String]), String, (StatusCode, ErrorResponse), ObjectUrl, Any, IO] =
byPathEndpoint(authorizationService, resourceGlobalName)
.get
.description("Retrieve an object by its path and return an url to download it")
Expand All @@ -88,7 +88,7 @@ object ObjectsEndpoints:
file => s3Service.presignedUrl(defaultBucket, file).pure[IO].map(ObjectUrl.apply)
}

private def listObjects(authorizationService: AuthorizationService, resourceGlobalName: String): PartialServerEndpoint[String, User, String, (StatusCode, ErrorResponse), Map[String, String], Any, IO] = byPathEndpoint(authorizationService, resourceGlobalName)
private def listObjects(authorizationService: AuthorizationService, resourceGlobalName: String): PartialServerEndpoint[String, (User, Option[String]), String, (StatusCode, ErrorResponse), Map[String, String], Any, IO] = byPathEndpoint(authorizationService, resourceGlobalName)
.description("Retrieve a list of objects by their path and return a list of download URLs for each object")
.post
.in("list")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ object PermissionsEndpoints:
private def listPermissionsServer(authorizationService: AuthorizationService): ServerEndpoint[Any, IO] =
listPermissions(authorizationService).serverLogicSuccess { user =>
_ =>
IO(user.permissions.map(_.resource_id).toList)
IO(user.permissions.map(_.rsid).toList)
}

def all(authorizationService: AuthorizationService): Seq[ServerEndpoint[Any, IO]] = List(
Expand Down
15 changes: 14 additions & 1 deletion src/main/scala/bio/ferlab/ferload/model/IntrospectResponse.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
package bio.ferlab.ferload.model

case class IntrospectResponse(active: Boolean, exp: Option[Int], iat: Option[Int], aud: Option[String], nbf: Option[Int], permissions: Option[Seq[Permissions]])
case class IntrospectResponse(
active: Boolean,
exp: Option[Int],
iat: Option[Int],
aud: Option[String],
sub: Option[String],
azp: Option[String],
nbf: Option[Int],
authorization: Option[Authorisation]
)

case class Authorisation(
permissions: Seq[Permissions]
)
2 changes: 1 addition & 1 deletion src/main/scala/bio/ferlab/ferload/model/Permissions.scala
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package bio.ferlab.ferload.model

case class Permissions(resource_id: String, rsname: Option[String], resource_scopes: Seq[String])
case class Permissions(rsid: String, rsname: Option[String], scopes: Seq[String])
12 changes: 11 additions & 1 deletion src/main/scala/bio/ferlab/ferload/model/drs/AccessURL.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ package bio.ferlab.ferload.model.drs
* @param headers An optional list of headers to include in the HTTP request to `url`. These headers can be used to provide auth tokens required to fetch the object bytes. for example: ''Authorization: Basic Z2E0Z2g6ZHJz''
*/
case class AccessURL (
url: String,
url: Option[String],
headers: Option[List[String]]
)

object AccessURL {
def build(url: String): AccessURL = {
AccessURL(
Some(url),
None
)
}

}

6 changes: 3 additions & 3 deletions src/main/scala/bio/ferlab/ferload/model/drs/DrsObject.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ case class DrsObject(
)

object DrsObject {
def build(resource: ReadResource, presignedUrl: String, host: String): DrsObject = {
def build(resource: ReadResource, access_id: String, host: String): DrsObject = {

val accessMethods = AccessMethod(
`type` = "https",
access_url = Some(AccessURL(
url = presignedUrl,
url = None,
headers = None
)),
access_id = None,
access_id = Some(access_id),
region = None,
authorizations = None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import sttp.model
import sttp.model.MediaType.ApplicationXWwwFormUrlencoded
import sttp.model.StatusCode

import java.lang

/**
* Service used to authorize a user to access resources
*
Expand Down Expand Up @@ -68,22 +70,26 @@ class AuthorizationService(authConfig: AuthConfig, backend: SttpBackend[IO, Fs2S
}

/**
* Introspect a party token to get the permissions associated with it.
* Introspect a token to get contents.
*
* @param partyToken the party token to introspect
* @param token the token to introspect
* @return the response from the introspection endpoint
*/
protected[services] def introspectPartyToken(partyToken: String): IO[IntrospectResponse] = {
protected[services] def introspectToken(token: String): IO[IntrospectResponse] = {

val introspect = basicRequest.post(uri"${authConfig.baseUri}/protocol/openid-connect/token/introspect")
.contentType(ApplicationXWwwFormUrlencoded)
.body("token_type_hint" -> "requesting_party_token",
"token" -> partyToken,
.body(
"token" -> token,
"client_id" -> authConfig.clientId,
"client_secret" -> authConfig.clientSecret)
.response(asJson[IntrospectResponse])
.send(backend)
introspect.flatMap(r => IO.fromEither(r.body))
introspect.flatMap(r => {
IO.fromEither(r.body)
}

)

}

Expand All @@ -94,22 +100,30 @@ class AuthorizationService(authConfig: AuthConfig, backend: SttpBackend[IO, Fs2S
* @param resources the resources to access
* @return the user with permissions if the token is valid and if user have access to the resources. Otherwise, return errors (Unauthorized, Forbidden, NotFound).
*/
def authLogic(token: String, resources: Seq[String]): IO[Either[(StatusCode, ErrorResponse), User]] = {
val r: IO[User] = for {
def authLogic(token: String, resources: Seq[String], accessId: Option[String] = None): IO[Either[(StatusCode, ErrorResponse), (User, Option[String])]] = {
val r: IO[(User, Option[String])] = for {
accessToken <- introspectToken(token)
partyToken <- requestPartyToken(token, resources)
permissionToken <- introspectPartyToken(partyToken)
permissionToken <- introspectToken(partyToken)
} yield {
val value: Set[Permissions] = permissionToken.permissions.map(_.toSet).getOrElse(Set.empty)
User(partyToken, value)

//Only request with token from the audience client is authorized
val isAuthorizedClientAccessToken = accessToken.azp.exists(_.equalsIgnoreCase(authConfig.audience.get))
if(!isAuthorizedClientAccessToken){
throw HttpError(s"Unauthorized client: ${accessToken.azp.getOrElse("Nothing")}", StatusCode.Forbidden)
}

val value: Set[Permissions] = permissionToken.authorization.map(_.permissions.toSet).getOrElse(Set.empty)
(User(partyToken, value), accessId)
}

r.map {
case User(_, permissions) if containAllPermissions(resources, permissions) => Right(User(token, permissions))
case User(_, permissions) => Left((StatusCode.Forbidden, ErrorResponse(resources.filterNot(permissions.map(_.resource_id).contains).mkString("[",",","]"), 403)))
case (User(_, permissions), accessId) if containAllPermissions(resources, permissions) => Right((User(token, permissions), accessId))
case (User(_, permissions), _) => Left((StatusCode.Forbidden, ErrorResponse(resources.filterNot(permissions.map(_.rsid).contains).mkString("[",",","]"), 403)))
}
.recover {
case HttpError(_, statusCode) if Seq(StatusCode.Unauthorized, StatusCode.Forbidden).contains(statusCode) => Left((statusCode, ErrorResponse("Unauthorized", statusCode.code))).withRight[User]
case e: HttpError[String] if e.statusCode == StatusCode.BadRequest && e.body.contains("invalid_resource") => Left((StatusCode.NotFound, ErrorResponse("Not Found", 404))).withRight[User]
case HttpError(_, statusCode) if Seq(StatusCode.Unauthorized, StatusCode.Forbidden).contains(statusCode) => Left((statusCode, ErrorResponse("Unauthorized", statusCode.code))).withRight[(User, Option[String])]
case e: HttpError[String] if e.statusCode == StatusCode.BadRequest && e.body.contains("invalid_resource") => Left((StatusCode.NotFound, ErrorResponse("Not Found", 404))).withRight[(User, Option[String])]
}

}
Expand All @@ -132,9 +146,9 @@ class AuthorizationService(authConfig: AuthConfig, backend: SttpBackend[IO, Fs2S

val r: IO[User] = for {
partyToken <- requestPartyToken(token, fileIds)
permissionToken <- introspectPartyToken(partyToken)
permissionToken <- introspectToken(partyToken)
} yield {
val value: Set[Permissions] = permissionToken.permissions.map(_.toSet).getOrElse(Set.empty)
val value: Set[Permissions] = permissionToken.authorization.map(au => au.permissions.toSet).getOrElse(Set.empty)
User(partyToken, value)
}

Expand Down Expand Up @@ -184,7 +198,6 @@ class AuthorizationService(authConfig: AuthConfig, backend: SttpBackend[IO, Fs2S
resourceInPermissions.contains(r)
})
}

}


Expand Down
Loading

0 comments on commit 4482478

Please sign in to comment.