diff --git a/src/main/scala/bio/ferlab/ferload/Config.scala b/src/main/scala/bio/ferlab/ferload/Config.scala index 9ff9adc..476014a 100644 --- a/src/main/scala/bio/ferlab/ferload/Config.scala +++ b/src/main/scala/bio/ferlab/ferload/Config.scala @@ -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, @@ -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"), - ) } } diff --git a/src/main/scala/bio/ferlab/ferload/endpoints/DrsEndpoints.scala b/src/main/scala/bio/ferlab/ferload/endpoints/DrsEndpoints.scala index 0e30e64..b43e6e9 100644 --- a/src/main/scala/bio/ferlab/ferload/endpoints/DrsEndpoints.scala +++ b/src/main/scala/bio/ferlab/ferload/endpoints/DrsEndpoints.scala @@ -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") @@ -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") @@ -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) => @@ -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) ) \ No newline at end of file diff --git a/src/main/scala/bio/ferlab/ferload/endpoints/LegacyObjectEndpoints.scala b/src/main/scala/bio/ferlab/ferload/endpoints/LegacyObjectEndpoints.scala index 01babf7..9ae4478 100644 --- a/src/main/scala/bio/ferlab/ferload/endpoints/LegacyObjectEndpoints.scala +++ b/src/main/scala/bio/ferlab/ferload/endpoints/LegacyObjectEndpoints.scala @@ -15,13 +15,13 @@ 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") @@ -29,7 +29,7 @@ object LegacyObjectEndpoints: .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() diff --git a/src/main/scala/bio/ferlab/ferload/endpoints/ObjectsEndpoints.scala b/src/main/scala/bio/ferlab/ferload/endpoints/ObjectsEndpoints.scala index 1c53a64..33de04d 100644 --- a/src/main/scala/bio/ferlab/ferload/endpoints/ObjectsEndpoints.scala +++ b/src/main/scala/bio/ferlab/ferload/endpoints/ObjectsEndpoints.scala @@ -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")) @@ -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) @@ -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 @@ -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") @@ -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") diff --git a/src/main/scala/bio/ferlab/ferload/endpoints/PermissionsEndpoints.scala b/src/main/scala/bio/ferlab/ferload/endpoints/PermissionsEndpoints.scala index b558c64..39bbdfa 100644 --- a/src/main/scala/bio/ferlab/ferload/endpoints/PermissionsEndpoints.scala +++ b/src/main/scala/bio/ferlab/ferload/endpoints/PermissionsEndpoints.scala @@ -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( diff --git a/src/main/scala/bio/ferlab/ferload/model/IntrospectResponse.scala b/src/main/scala/bio/ferlab/ferload/model/IntrospectResponse.scala index 1dfd6f7..1691f4d 100644 --- a/src/main/scala/bio/ferlab/ferload/model/IntrospectResponse.scala +++ b/src/main/scala/bio/ferlab/ferload/model/IntrospectResponse.scala @@ -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]]) \ No newline at end of file +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] + ) \ No newline at end of file diff --git a/src/main/scala/bio/ferlab/ferload/model/Permissions.scala b/src/main/scala/bio/ferlab/ferload/model/Permissions.scala index ee4281a..c490931 100644 --- a/src/main/scala/bio/ferlab/ferload/model/Permissions.scala +++ b/src/main/scala/bio/ferlab/ferload/model/Permissions.scala @@ -1,3 +1,3 @@ package bio.ferlab.ferload.model -case class Permissions(resource_id: String, rsname: Option[String], resource_scopes: Seq[String]) \ No newline at end of file +case class Permissions(rsid: String, rsname: Option[String], scopes: Seq[String]) \ No newline at end of file diff --git a/src/main/scala/bio/ferlab/ferload/model/drs/AccessURL.scala b/src/main/scala/bio/ferlab/ferload/model/drs/AccessURL.scala index 8256b9b..3cacc79 100644 --- a/src/main/scala/bio/ferlab/ferload/model/drs/AccessURL.scala +++ b/src/main/scala/bio/ferlab/ferload/model/drs/AccessURL.scala @@ -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 + ) + } + +} + diff --git a/src/main/scala/bio/ferlab/ferload/model/drs/DrsObject.scala b/src/main/scala/bio/ferlab/ferload/model/drs/DrsObject.scala index 6f3f1f8..d840521 100644 --- a/src/main/scala/bio/ferlab/ferload/model/drs/DrsObject.scala +++ b/src/main/scala/bio/ferlab/ferload/model/drs/DrsObject.scala @@ -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 diff --git a/src/main/scala/bio/ferlab/ferload/services/AuthorizationService.scala b/src/main/scala/bio/ferlab/ferload/services/AuthorizationService.scala index 9e9038e..5c8ef47 100644 --- a/src/main/scala/bio/ferlab/ferload/services/AuthorizationService.scala +++ b/src/main/scala/bio/ferlab/ferload/services/AuthorizationService.scala @@ -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 * @@ -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) + } + + ) } @@ -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])] } } @@ -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) } @@ -184,7 +198,6 @@ class AuthorizationService(authConfig: AuthConfig, backend: SttpBackend[IO, Fs2S resourceInPermissions.contains(r) }) } - } diff --git a/src/test/scala/bio/ferlab/ferload/endpoints/ConfigEndpointsSpec.scala b/src/test/scala/bio/ferlab/ferload/endpoints/ConfigEndpointsSpec.scala index 22ffa28..2711231 100644 --- a/src/test/scala/bio/ferlab/ferload/endpoints/ConfigEndpointsSpec.scala +++ b/src/test/scala/bio/ferlab/ferload/endpoints/ConfigEndpointsSpec.scala @@ -22,7 +22,7 @@ class ConfigEndpointsSpec extends AnyFlatSpec with Matchers with EitherValues: AuthConfig("http://localhost:8080", "realm", "clientId", "clientSecret", None, None), HttpConfig("localhost", 9090), S3Config(Some("accessKey"), Some("secretKey"), Some("endpoint"), Some("bucket"), false, Some("region"), 3600), - DrsConfig("ferlaod", "Ferload", "ferload.ferlab.bio", "1.3.0", "Ferlab", "https://ferlab.bio"), + DrsConfig("ferlaod", "Ferload", "ferload.ferlab.bio", "1.3.0", "Ferlab", "https://ferlab.bio", accessId = "accessId"), FerloadClientConfig(FerloadClientConfig.PASSWORD, "ferloadClientId", None, None) ) val backendStub = TapirStubInterpreter(SttpBackendStub(new CatsMonadError[IO]())) @@ -44,7 +44,7 @@ class ConfigEndpointsSpec extends AnyFlatSpec with Matchers with EitherValues: AuthConfig("http://localhost:8080", "realm", "clientId", "clientSecret", None, None), HttpConfig("localhost", 9090), S3Config(Some("accessKey"), Some("secretKey"), Some("endpoint"), Some("bucket"), false, Some("region"), 3600), - DrsConfig("ferlaod", "Ferload", "ferload.ferlab.bio", "1.3.0", "Ferlab", "https://ferlab.bio"), + DrsConfig("ferlaod", "Ferload", "ferload.ferlab.bio", "1.3.0", "Ferlab", "https://ferlab.bio", accessId = "accessId"), FerloadClientConfig(FerloadClientConfig.TOKEN, "ferloadClientId", Some("https://ferload.ferlab.bio/token"), Some("Please copy / paste this url in your browser to get a new authentication token.")) ) val backendStub = TapirStubInterpreter(SttpBackendStub(new CatsMonadError[IO]())) @@ -77,7 +77,7 @@ class ConfigEndpointsSpec extends AnyFlatSpec with Matchers with EitherValues: AuthConfig("http://localhost:8080", "realm", "resource_client", "clientSecret", Some("cqdg_acl"), None), HttpConfig("localhost", 9090), S3Config(Some("accessKey"), Some("secretKey"), Some("endpoint"), Some("bucket"), false, Some("region"), 3600), - DrsConfig("ferlaod", "Ferload", "ferload.ferlab.bio", "1.3.0", "Ferlab", "https://ferlab.bio"), + DrsConfig("ferlaod", "Ferload", "ferload.ferlab.bio", "1.3.0", "Ferlab", "https://ferlab.bio", accessId = "accessId"), FerloadClientConfig(FerloadClientConfig.DEVICE, "ferloadClientId", Some("https://ferload.ferlab.bio/token"), Some("Please copy / paste this url in your browser to get a new authentication token.")) ) val backendStub = TapirStubInterpreter(SttpBackendStub(new CatsMonadError[IO]())) diff --git a/src/test/scala/bio/ferlab/ferload/services/AuthorizationServiceSpec.scala b/src/test/scala/bio/ferlab/ferload/services/AuthorizationServiceSpec.scala index 6d5925c..7a80a4c 100644 --- a/src/test/scala/bio/ferlab/ferload/services/AuthorizationServiceSpec.scala +++ b/src/test/scala/bio/ferlab/ferload/services/AuthorizationServiceSpec.scala @@ -2,7 +2,7 @@ package bio.ferlab.ferload.services import bio.ferlab.ferload.AuthConfig import bio.ferlab.ferload.unwrap -import bio.ferlab.ferload.model.{ErrorResponse, IntrospectResponse, Permissions, User} +import bio.ferlab.ferload.model.{Authorisation, ErrorResponse, IntrospectResponse, Permissions, User} import cats.effect.IO import io.circe.Json import io.circe.parser.parse @@ -58,21 +58,23 @@ class AuthorizationServiceSpec extends AnyFlatSpec with Matchers with EitherValu | "iat": 20, | "aud" : "cqdg", | "nbf": 4, + | "authorization":{ | "permissions" : [ | { - | "resource_id": "F1", + | "rsid": "F1", | "rsname": "F1 Name", - | "resource_scopes": ["Scope1", "Scope2"] + | "scopes": ["Scope1", "Scope2"] | } | ] + | } |} """.stripMargin) ) val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", None, None) val authorizationService = new AuthorizationService(authConfig, testingBackend) - val resp = authorizationService.introspectPartyToken("E123456").unwrap - resp shouldBe IntrospectResponse(active = true, exp = Some(65), iat = Some(20), aud = Some("cqdg"), nbf = Some(4), permissions = Some(Seq(Permissions("F1", Some("F1 Name"), Seq("Scope1", "Scope2"))))) + val resp = authorizationService.introspectToken("E123456").unwrap + resp shouldBe IntrospectResponse(active = true, exp = Some(65), iat = Some(20), aud = Some("cqdg"), sub = None, azp = None, nbf = Some(4), authorization = Some(Authorisation(Seq(Permissions("F1", Some("F1 Name"), Seq("Scope1", "Scope2")))))) } @@ -89,21 +91,24 @@ class AuthorizationServiceSpec extends AnyFlatSpec with Matchers with EitherValu | "iat": 20, | "aud" : "cqdg", | "nbf": 4, + | "azp": "allowed_client", + | "authorization":{ | "permissions" : [ | { - | "resource_id": "F1", + | "rsid": "F1", | "rsname": "F1", - | "resource_scopes": ["Scope1", "Scope2"] + | "scopes": ["Scope1", "Scope2"] | } | ] + | } |} """.stripMargin) ) - val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", None, None) + val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", Some("allowed_client"), None) val authorizationService = new AuthorizationService(authConfig, testingBackend) - authorizationService.authLogic("token", Seq("F1")).unwrap.value shouldBe User("token", Set(Permissions("F1", Some("F1"), Seq("Scope1", "Scope2")))) + authorizationService.authLogic("token", Seq("F1")).unwrap.value._1 shouldBe User("token", Set(Permissions("F1", Some("F1"), Seq("Scope1", "Scope2")))) } it should "return a forbidden if user dont have access to all resources" in { @@ -119,18 +124,21 @@ class AuthorizationServiceSpec extends AnyFlatSpec with Matchers with EitherValu | "exp": 65, | "iat": 20, | "aud" : "cqdg", + | "azp": "allowed_client", | "nbf": 4, + | "authorization":{ | "permissions" : [ | { - | "resource_id": "F1", + | "rsid": "F1", | "rsname": "F1", - | "resource_scopes": ["Scope1", "Scope2"] + | "scopes": ["Scope1", "Scope2"] | } | ] + | } |} """.stripMargin) ) - val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", None, None) + val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", Some("allowed_client"), None) val authorizationService = new AuthorizationService(authConfig, testingBackend) @@ -154,16 +162,19 @@ class AuthorizationServiceSpec extends AnyFlatSpec with Matchers with EitherValu | "iat": 20, | "aud" : "cqdg", | "nbf": 4, + | "azp": "allowed_client", + | "authorization":{ | "permissions" : [ | { - | "resource_id": "F1", + | "rsid": "F1", | "rsname": "F1", - | "resource_scopes": ["Scope1", "Scope2"] + | "scopes": ["Scope1", "Scope2"] | } | ] + | } |} """.stripMargin) ) - val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", None, None) + val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", Some("allowed_client"), None) val authorizationService = new AuthorizationService(authConfig, testingBackend) @@ -188,16 +199,19 @@ class AuthorizationServiceSpec extends AnyFlatSpec with Matchers with EitherValu | "iat": 20, | "aud" : "cqdg", | "nbf": 4, + | "azp": "allowed_client", + | "authorization":{ | "permissions" : [ | { - | "resource_id": "F1", + | "rsid": "F1", | "rsname": "F1", - | "resource_scopes": ["Scope1", "Scope2"] + | "scopes": ["Scope1", "Scope2"] | } | ] + | } |} """.stripMargin) ) - val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", None, None) + val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", Some("allowed_client"), None) val authorizationService = new AuthorizationService(authConfig, testingBackend) @@ -222,22 +236,66 @@ class AuthorizationServiceSpec extends AnyFlatSpec with Matchers with EitherValu | "iat": 20, | "aud" : "cqdg", | "nbf": 4, + | "azp": "allowed_client", + | "authorization":{ | "permissions" : [ | { - | "resource_id": "F1", + | "rsid": "F1", | "rsname": "F1", - | "resource_scopes": ["Scope1", "Scope2"] + | "scopes": ["Scope1", "Scope2"] | } | ] + | } |} """.stripMargin) ) - val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", None, None) + val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", Some("allowed_client"), None) val authorizationService = new AuthorizationService(authConfig, testingBackend) authorizationService.authLogic("token", Seq("F1")).unwrap.left.value shouldBe(StatusCode.Unauthorized, ErrorResponse("Unauthorized", 401)) } + // authLogic does return pre-sign url, only authorized client should be granted access + it should "return forbidden for any client other then the authorized client" in { + val testingBackend = new RecordingSttpBackend(Http4sBackend.stub[IO] + .whenRequestMatches(r => r.uri.path == Seq("realms", "realm", "protocol", "openid-connect", "token")) + .thenRespond(""" {"access_token": "E123456", "expires_in": 65, "refresh_expires_in": 0, "token_type" : "bearer"} """) + .whenRequestMatches(r => r.uri.path.contains("introspect")) + .thenRespond( + """ { + | "active": true, + | "exp": 65, + | "iat": 20, + | "aud" : "cqdg", + | "nbf": 4, + | "azp": "allowed_client", + | "authorization":{ + | "permissions" : [ + | { + | "rsid": "F1", + | "rsname": "F1", + | "scopes": ["Scope1", "Scope2"] + | } + | ] + | } + |} """.stripMargin) + ) + val authConfigOk = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", Some("allowed_client"), None) + + val authorizationServiceOk = new AuthorizationService(authConfigOk, testingBackend) + + // match client: allowed_client + authorizationServiceOk.authLogic("token", Seq("F1")).unwrap.value._1 shouldBe User("token", Set(Permissions("F1", Some("F1"), Seq("Scope1", "Scope2")))) + + val authConfigNotOk = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", Some("other_allowed_client"), None) + + val authorizationServiceNotOk = new AuthorizationService(authConfigNotOk, testingBackend) + + // not matching clients: allowed_client vs. other_allowed_client + authorizationServiceNotOk.authLogic("token", Seq("F1")).unwrap.left.value shouldBe(StatusCode.Forbidden, ErrorResponse("Unauthorized", 403)) + + } + "authLogicAuthorizationForUser" should "return a User" in { val testingBackend = new RecordingSttpBackend(Http4sBackend.stub[IO] .whenRequestMatches(r => r.uri.path == Seq("realms", "realm", "protocol", "openid-connect", "token")) @@ -250,17 +308,20 @@ class AuthorizationServiceSpec extends AnyFlatSpec with Matchers with EitherValu | "iat": 20, | "aud" : "cqdg", | "nbf": 4, + | "azp": "allowed_client", + | "authorization":{ | "permissions" : [ | { - | "resource_id": "F1", + | "rsid": "F1", | "rsname": "F1", - | "resource_scopes": ["Scope1", "Scope2"] + | "scopes": ["Scope1", "Scope2"] | } | ] + | } |} """.stripMargin) ) - val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", None, None) + val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", Some("allowed_client"), None) val authorizationService = new AuthorizationService(authConfig, testingBackend) @@ -283,17 +344,20 @@ class AuthorizationServiceSpec extends AnyFlatSpec with Matchers with EitherValu | "iat": 20, | "aud" : "cqdg", | "nbf": 4, + | "azp": "allowed_client", + | "authorization":{ | "permissions" : [ | { - | "resource_id": "F1", + | "rsid": "F1", | "rsname": "F1", - | "resource_scopes": ["Scope1", "Scope2"] + | "scopes": ["Scope1", "Scope2"] | } | ] + | } |} """.stripMargin) ) - val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", None, None) + val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", Some("allowed_client"), None) val authorizationService = new AuthorizationService(authConfig, testingBackend) @@ -320,16 +384,19 @@ class AuthorizationServiceSpec extends AnyFlatSpec with Matchers with EitherValu | "iat": 20, | "aud" : "cqdg", | "nbf": 4, + | "azp": "allowed_client", + | "authorization":{ | "permissions" : [ | { - | "resource_id": "F1", + | "rsid": "F1", | "rsname": "F1", - | "resource_scopes": ["Scope1", "Scope2"] + | "scopes": ["Scope1", "Scope2"] | } | ] + | } |} """.stripMargin) ) - val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", None, None) + val authConfig = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", Some("allowed_client"), None) val authorizationService = new AuthorizationService(authConfig, testingBackend) @@ -337,4 +404,51 @@ class AuthorizationServiceSpec extends AnyFlatSpec with Matchers with EitherValu authorizationService.authLogicAuthorizationForUser("token", inputJson).unwrap.left.value shouldBe(StatusCode.Unauthorized, ErrorResponse("Unauthorized", 401)) } + + // authLogicAuthorizationForUser does not return any pre-sign url, all valid clients should be able to access + it should "authorize for any valid client" in { + val testingBackend = new RecordingSttpBackend(Http4sBackend.stub[IO] + .whenRequestMatches(r => { + r.uri.path == Seq("realms", "realm", "protocol", "openid-connect", "token") + }) + .thenRespond(""" {"access_token": "E123456", "expires_in": 65, "refresh_expires_in": 0, "token_type" : "bearer"} """) + .whenRequestMatches(r => r.uri.path.contains("introspect")) + .thenRespond( + """ { + | "active": true, + | "exp": 65, + | "iat": 20, + | "aud" : "cqdg", + | "nbf": 4, + | "azp": "allowed_client", + | "authorization":{ + | "permissions" : [ + | { + | "rsid": "F1", + | "rsname": "F1", + | "scopes": ["Scope1", "Scope2"] + | } + | ] + | } + |} """.stripMargin) + ) + val authConfigOne = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", Some("allowed_client"), None) + + val authorizationServiceOne = new AuthorizationService(authConfigOne, testingBackend) + + val inputJsonOne = parse("""{"file_ids":["FI1"]}""".stripMargin).getOrElse(Json.Null) + + authorizationServiceOne.authLogicAuthorizationForUser("token", inputJsonOne).unwrap.value shouldBe User("token", Set(Permissions("F1", Some("F1"), Seq("Scope1", "Scope2")))) + + //------------------------------------ + + val authConfigTwo = AuthConfig("http://stub.local", "realm", "clientId", "clientSecret", Some("other_allowed_client"), None) + + val authorizationServiceTwo = new AuthorizationService(authConfigTwo, testingBackend) + + val inputJsonTwo = parse("""{"file_ids":["FI1"]}""".stripMargin).getOrElse(Json.Null) + + authorizationServiceTwo.authLogicAuthorizationForUser("token", inputJsonTwo).unwrap.value shouldBe User("token", Set(Permissions("F1", Some("F1"), Seq("Scope1", "Scope2")))) + + } }