From 5a71d0074285f44bc7fa0adfb90efd469c20ff83 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Mon, 1 May 2017 16:57:34 -0700 Subject: Change signature for GET query params to allow for duplicate query keys --- src/main/scala/xyz/driver/core/rest.scala | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) (limited to 'src/main/scala/xyz/driver/core/rest.scala') diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index f1eab45..0bc9595 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -217,11 +217,8 @@ package rest { protected def jsonEntity(json: JsValue): RequestEntity = HttpEntity(ContentTypes.`application/json`, json.compactPrint) - protected def get(baseUri: Uri, path: String) = - HttpRequest(HttpMethods.GET, endpointUri(baseUri, path)) - - protected def get(baseUri: Uri, path: String, query: Map[String, String]) = - HttpRequest(HttpMethods.GET, endpointUri(baseUri, path, query)) + protected def get(baseUri: Uri, path: String, query: (String, String)*) = + HttpRequest(HttpMethods.GET, endpointUri(baseUri, path, query: _*)) protected def post(baseUri: Uri, path: String, httpEntity: RequestEntity) = HttpRequest(HttpMethods.POST, endpointUri(baseUri, path), entity = httpEntity) @@ -235,8 +232,8 @@ package rest { protected def endpointUri(baseUri: Uri, path: String) = baseUri.withPath(Uri.Path(path)) - protected def endpointUri(baseUri: Uri, path: String, query: Map[String, String]) = - baseUri.withPath(Uri.Path(path)).withQuery(Uri.Query(query)) + protected def endpointUri(baseUri: Uri, path: String, query: (String, String)*) = + baseUri.withPath(Uri.Path(path)).withQuery(Uri.Query(query: _*)) } trait ServiceTransport { -- cgit v1.2.3 From e6858db64f9a1c0121aed972cf7426a746eb7175 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Wed, 3 May 2017 17:42:45 -0700 Subject: Update AuthProvider to use cached permissions token --- build.sbt | 1 + src/main/scala/xyz/driver/core/auth.scala | 1 + src/main/scala/xyz/driver/core/rest.scala | 157 ++++++++++++++++---------- src/test/scala/xyz/driver/core/AuthTest.scala | 71 ++++++++---- 4 files changed, 151 insertions(+), 79 deletions(-) (limited to 'src/main/scala/xyz/driver/core/rest.scala') diff --git a/build.sbt b/build.sbt index c4de456..0963f6c 100644 --- a/build.sbt +++ b/build.sbt @@ -10,6 +10,7 @@ lazy val core = (project in file(".")) "com.typesafe.akka" %% "akka-http-core" % akkaHttpV, "com.typesafe.akka" %% "akka-http-spray-json" % akkaHttpV, "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpV, + "com.pauldijou" %% "jwt-core" % "0.9.2", "org.scalatest" % "scalatest_2.11" % "2.2.6" % "test", "org.scalacheck" %% "scalacheck" % "1.12.5" % "test", "org.mockito" % "mockito-core" % "1.9.5" % "test", diff --git a/src/main/scala/xyz/driver/core/auth.scala b/src/main/scala/xyz/driver/core/auth.scala index f9a1a57..5dea2db 100644 --- a/src/main/scala/xyz/driver/core/auth.scala +++ b/src/main/scala/xyz/driver/core/auth.scala @@ -23,6 +23,7 @@ object auth { final case class AuthToken(value: String) final case class RefreshToken(value: String) + final case class PermissionsToken(value: String) final case class PasswordHash(value: String) diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index 0bc9595..4e965aa 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -5,17 +5,17 @@ import akka.http.scaladsl.Http import akka.http.scaladsl.model._ import akka.http.scaladsl.model.headers.{HttpChallenges, RawHeader} import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected -import akka.http.scaladsl.server.Directive0 -import com.typesafe.scalalogging.Logger -import akka.http.scaladsl.unmarshalling.Unmarshal -import akka.http.scaladsl.unmarshalling.Unmarshaller +import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller} import akka.stream.ActorMaterializer import akka.stream.scaladsl.Flow import akka.util.ByteString import com.github.swagger.akka.model._ import com.github.swagger.akka.{HasActorSystem, SwaggerHttpService} import com.typesafe.config.Config +import com.typesafe.scalalogging.Logger import io.swagger.models.Scheme +import java.security.PublicKey +import pdi.jwt.{Jwt, JwtAlgorithm} import xyz.driver.core.auth._ import xyz.driver.core.time.provider.TimeProvider @@ -27,13 +27,13 @@ import scalaz.{ListT, OptionT} package rest { object `package` { - import akka.http.scaladsl.server._ + import akka.http.scaladsl.server.{RequestContext => _, _} import Directives._ - def serviceContext: Directive1[ServiceRequestContext] = extract(ctx => extractServiceContext(ctx.request)) + def serviceContext: Directive1[RequestContext] = extract(ctx => extractServiceContext(ctx.request)) - def extractServiceContext(request: HttpRequest): ServiceRequestContext = - ServiceRequestContext(extractTrackingId(request), extractContextHeaders(request)) + def extractServiceContext(request: HttpRequest): RequestContext = + new RequestContext(extractTrackingId(request), extractContextHeaders(request)) def extractTrackingId(request: HttpRequest): String = { request.headers @@ -43,7 +43,8 @@ package rest { def extractContextHeaders(request: HttpRequest): Map[String, String] = { request.headers.filter { h => - h.name === ContextHeaders.AuthenticationTokenHeader || h.name === ContextHeaders.TrackingIdHeader + h.name === ContextHeaders.AuthenticationTokenHeader || h.name === ContextHeaders.TrackingIdHeader || + h.name === ContextHeaders.PermissionsTokenHeader } map { header => if (header.name === ContextHeaders.AuthenticationTokenHeader) { header.name -> header.value.stripPrefix(ContextHeaders.AuthenticationHeaderPrefix).trim @@ -91,14 +92,31 @@ package rest { } } - final case class ServiceRequestContext(trackingId: String = generators.nextUuid().toString, - contextHeaders: Map[String, String] = Map.empty[String, String]) { - + class RequestContext(val trackingId: String = generators.nextUuid().toString, + val contextHeaders: Map[String, String] = Map.empty[String, String]) { def authToken: Option[AuthToken] = contextHeaders.get(AuthProvider.AuthenticationTokenHeader).map(AuthToken.apply) - def withAuthToken(authToken: AuthToken): ServiceRequestContext = - copy(contextHeaders = contextHeaders.updated(AuthProvider.AuthenticationTokenHeader, authToken.value)) + def permissionsToken: Option[PermissionsToken] = + contextHeaders.get(AuthProvider.PermissionsTokenHeader).map(PermissionsToken.apply) + + def withAuthenticatedUser[U <: User](authToken: AuthToken, user: U): AuthenticatedRequestContext[U] = + new AuthenticatedRequestContext(trackingId, + contextHeaders.updated(AuthProvider.AuthenticationTokenHeader, authToken.value), + user) + } + + class AuthenticatedRequestContext[U <: User](override val trackingId: String = generators.nextUuid().toString, + override val contextHeaders: Map[String, String] = + Map.empty[String, String], + val authenticatedUser: U) + extends RequestContext { + + def withPermissionsToken(permissionsToken: PermissionsToken): AuthenticatedRequestContext[U] = + new AuthenticatedRequestContext[U]( + trackingId, + contextHeaders.updated(AuthProvider.PermissionsTokenHeader, permissionsToken.value), + authenticatedUser) } object ContextHeaders { @@ -115,21 +133,24 @@ package rest { val SetPermissionsTokenHeader = "set-permissions" } - trait Authorization { - def userHasPermission(user: User, permission: Permission)(implicit ctx: ServiceRequestContext): Future[Boolean] + trait Authorization[U <: User] { + def userHasPermissions(permissions: Seq[Permission])( + implicit ctx: AuthenticatedRequestContext[U]): OptionT[Future, + (Map[Permission, Boolean], PermissionsToken)] } - class AlwaysAllowAuthorization extends Authorization { - override def userHasPermission(user: User, permission: Permission)( - implicit ctx: ServiceRequestContext): Future[Boolean] = { - Future.successful(true) - } + class AlwaysAllowAuthorization[U <: User] extends Authorization[U] { + override def userHasPermissions(permissions: Seq[Permission])( + implicit ctx: AuthenticatedRequestContext[U]): OptionT[Future, + (Map[Permission, Boolean], PermissionsToken)] = + OptionT.optionT(Future.successful(Option((permissions.map(_ -> true).toMap, PermissionsToken(""))))) } - abstract class AuthProvider[U <: User](val authorization: Authorization, log: Logger)( - implicit execution: ExecutionContext) { + abstract class AuthProvider[U <: User](val authorization: Authorization[U], + val permissionsTokenPublicKey: PublicKey, + log: Logger)(implicit execution: ExecutionContext) { - import akka.http.scaladsl.server._ + import akka.http.scaladsl.server.{RequestContext => _, _} import Directives._ /** @@ -139,52 +160,69 @@ package rest { * @param ctx set of request values which can be relevant to authenticate user * @return authenticated user */ - def authenticatedUser(implicit ctx: ServiceRequestContext): OptionT[Future, U] - - /** - * Specific implementation can verify session expiration and single sign out - * to verify if session is still valid - */ - def isSessionValid(user: U)(implicit ctx: ServiceRequestContext): Future[Boolean] + def authenticatedUser(implicit ctx: RequestContext): OptionT[Future, U] /** * Verifies if request is authenticated and authorized to have `permissions` */ - def authorize(permissions: Permission*): Directive1[U] = { + def authorize(permissions: Permission*): Directive1[AuthenticatedRequestContext[U]] = { serviceContext flatMap { ctx => - onComplete(authenticatedUser(ctx).run flatMap { userOption => - userOption.traverseM[Future, (U, Boolean)] { user => - isSessionValid(user)(ctx).flatMap { sessionValid => - if (sessionValid) { - permissions.toList - .traverse[Future, Boolean](authorization.userHasPermission(user, _)(ctx)) - .map(results => Option(user -> results.forall(identity))) - } else { - Future.successful(Option.empty[(U, Boolean)]) - } - } - } - }).flatMap { - case Success(Some((user, authorizationResult))) => - if (authorizationResult) provide(user) - else { - val challenge = - HttpChallenges.basic(s"User does not have the required permissions: ${permissions.mkString(", ")}") - log.warn(s"User $user does not have the required permissions: ${permissions.mkString(", ")}") - reject(AuthenticationFailedRejection(CredentialsRejected, challenge)) - } - + onComplete { + (for { + authToken <- OptionT.optionT(Future.successful(ctx.authToken)) + user <- authenticatedUser(ctx) + authCtx = ctx.withAuthenticatedUser(authToken, user) + (authorizationResult, permissionsToken) <- userHasPermission(user, permissions)(authCtx) + } yield (authCtx.withPermissionsToken(permissionsToken), authorizationResult)).run + } flatMap { + case Success(Some((authCtx, true))) => provide(authCtx) + case Success(Some((authCtx, false))) => + val challenge = + HttpChallenges.basic(s"User does not have the required permissions: ${permissions.mkString(", ")}") + log.warn( + s"User ${authCtx.authenticatedUser} does not have the required permissions: ${permissions.mkString(", ")}") + reject(AuthenticationFailedRejection(CredentialsRejected, challenge)) case Success(None) => log.warn( s"Wasn't able to find authenticated user for the token provided to verify ${permissions.mkString(", ")}") reject(ValidationRejection(s"Wasn't able to find authenticated user for the token provided")) - case Failure(t) => log.warn(s"Wasn't able to verify token for authenticated user to verify ${permissions.mkString(", ")}", t) reject(ValidationRejection(s"Wasn't able to verify token for authenticated user", Some(t))) } } } + + protected def userHasPermission(user: U, permissions: Seq[Permission])( + ctx: AuthenticatedRequestContext[U]): OptionT[Future, (Boolean, PermissionsToken)] = { + import spray.json._ + + def authorizedByToken: OptionT[Future, (Boolean, PermissionsToken)] = { + OptionT.optionT(Future.successful(for { + token <- ctx.permissionsToken + jwt <- Jwt.decode(token.value, permissionsTokenPublicKey, Seq(JwtAlgorithm.RS256)).toOption + jwtJson = jwt.parseJson.asJsObject + + // Ensure jwt is for the currently authenticated user, otherwise return None to call permissions service + _ <- jwtJson.fields.get("sub").contains(JsString(user.id.value)).option(()) + + permissionsMap <- jwtJson.fields.get("permissions").map(_.asJsObject.fields) + + // Ensure all permissions are in the token, otherwise return none to call permissions service + _ <- permissions.forall(p => permissionsMap.contains(p.toString)).option(()) + + authorized = permissions.forall(p => permissionsMap.get(p.toString).contains(JsBoolean(true))) + } yield (authorized, token))) + } + + def authorizedByService: OptionT[Future, (Boolean, PermissionsToken)] = + authorization.userHasPermissions(permissions)(ctx).map { + case (permissionMap, token) => + (permissions.forall(p => permissionMap.getOrElse(p, false)), token) + } + + authorizedByToken.orElse(authorizedByService) + } } trait Service @@ -193,7 +231,6 @@ package rest { import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ import spray.json._ - import DefaultJsonProtocol._ protected implicit val exec: ExecutionContext protected implicit val materializer: ActorMaterializer @@ -238,9 +275,9 @@ package rest { trait ServiceTransport { - def sendRequestGetResponse(context: ServiceRequestContext)(requestStub: HttpRequest): Future[HttpResponse] + def sendRequestGetResponse(context: RequestContext)(requestStub: HttpRequest): Future[HttpResponse] - def sendRequest(context: ServiceRequestContext)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] + def sendRequest(context: RequestContext)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] } trait ServiceDiscovery { @@ -257,7 +294,7 @@ package rest { protected implicit val materializer = ActorMaterializer()(actorSystem) protected implicit val execution = executionContext - def sendRequestGetResponse(context: ServiceRequestContext)(requestStub: HttpRequest): Future[HttpResponse] = { + def sendRequestGetResponse(context: RequestContext)(requestStub: HttpRequest): Future[HttpResponse] = { val requestTime = time.currentTime() @@ -285,7 +322,7 @@ package rest { response } - def sendRequest(context: ServiceRequestContext)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] = { + def sendRequest(context: RequestContext)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] = { sendRequestGetResponse(context)(requestStub) map { response => if (response.status == StatusCodes.NotFound) { diff --git a/src/test/scala/xyz/driver/core/AuthTest.scala b/src/test/scala/xyz/driver/core/AuthTest.scala index ad8cec8..441b9c8 100644 --- a/src/test/scala/xyz/driver/core/AuthTest.scala +++ b/src/test/scala/xyz/driver/core/AuthTest.scala @@ -3,39 +3,49 @@ package xyz.driver.core import akka.http.scaladsl.model.headers.{HttpChallenges, RawHeader} import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server._ +import akka.http.scaladsl.server.{RequestContext => _, _} import akka.http.scaladsl.testkit.ScalatestRouteTest import org.scalatest.mock.MockitoSugar import org.scalatest.{FlatSpec, Matchers} +import pdi.jwt.{Jwt, JwtAlgorithm} import xyz.driver.core.auth._ import xyz.driver.core.logging._ -import xyz.driver.core.rest.{AuthProvider, Authorization, ServiceRequestContext} +import xyz.driver.core.rest.{AuthProvider, AuthenticatedRequestContext, Authorization, RequestContext} import scala.concurrent.Future import scalaz.OptionT class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRouteTest { - case object TestRoleAllowedPermission extends Permission - case object TestRoleNotAllowedPermission extends Permission + case object TestRoleAllowedPermission extends Permission + case object TestRoleAllowedByTokenPermission extends Permission + case object TestRoleNotAllowedPermission extends Permission val TestRole = Role(Id("1"), Name("testRole")) - implicit val exec = scala.concurrent.ExecutionContext.global + val (publicKey, privateKey) = { + import java.security.KeyPairGenerator - val authorization: Authorization = new Authorization { - override def userHasPermission(user: User, permission: Permission)( - implicit ctx: ServiceRequestContext): Future[Boolean] = { - Future.successful(permission === TestRoleAllowedPermission) - } + val keygen = KeyPairGenerator.getInstance("RSA") + keygen.initialize(2048) + + val keyPair = keygen.generateKeyPair() + (keyPair.getPublic, keyPair.getPrivate) } - val authStatusService = new AuthProvider[User](authorization, NoLogger) { + val authorization: Authorization[User] = new Authorization[User] { - override def isSessionValid(user: User)(implicit ctx: ServiceRequestContext): Future[Boolean] = - Future.successful(true) + override def userHasPermissions(permissions: Seq[Permission])( + implicit ctx: AuthenticatedRequestContext[User]): OptionT[Future, + (Map[Permission, Boolean], PermissionsToken)] = { + val permissionsMap = permissions.map(p => p -> (p === TestRoleAllowedPermission)).toMap + val token = PermissionsToken("TODO") + OptionT.optionT(Future.successful(Option((permissionsMap, token)))) + } + } - override def authenticatedUser(implicit ctx: ServiceRequestContext): OptionT[Future, User] = + val authStatusService = new AuthProvider[User](authorization, publicKey, NoLogger) { + override def authenticatedUser(implicit ctx: RequestContext): OptionT[Future, User] = OptionT.optionT[Future] { if (ctx.contextHeaders.keySet.contains(AuthProvider.AuthenticationTokenHeader)) { Future.successful(Some(BasicUser(Id[User]("1"), Set(TestRole)))) @@ -47,7 +57,7 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo import authStatusService._ - "'authorize' directive" should "throw error is auth token is not in the request" in { + "'authorize' directive" should "throw error if auth token is not in the request" in { Get("/naive/attempt") ~> authorize(TestRoleAllowedPermission) { user => @@ -59,7 +69,7 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo } } - it should "throw error is authorized user is not having the requested permission" in { + it should "throw error if authorized user does not have the requested permission" in { val referenceAuthToken = AuthToken("I am a test role's token") @@ -85,12 +95,35 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo Get("/valid/attempt/?a=2&b=5").addHeader( RawHeader(AuthProvider.AuthenticationTokenHeader, referenceAuthToken.value) ) ~> - authorize(TestRoleAllowedPermission) { user => - complete("Alright, user \"" + user.id + "\" is authorized") + authorize(TestRoleAllowedPermission) { ctx => + complete(s"Alright, user ${ctx.authenticatedUser.id} is authorized") + } ~> + check { + handled shouldBe true + responseAs[String] shouldBe "Alright, user 1 is authorized" + } + } + + it should "authorize permission found in permissions token" in { + import spray.json._ + + val claim = JsObject(Map( + "iss" -> JsString("users"), + "sub" -> JsString("1"), + "permissions" -> JsObject(Map(TestRoleAllowedByTokenPermission.toString -> JsBoolean(true))) + )).prettyPrint + val permissionsToken = PermissionsToken(Jwt.encode(claim, privateKey, JwtAlgorithm.RS256)) + val referenceAuthToken = AuthToken("I am token") + + Get("/alic/attempt/?a=2&b=5") + .addHeader(RawHeader(AuthProvider.AuthenticationTokenHeader, referenceAuthToken.value)) + .addHeader(RawHeader(AuthProvider.PermissionsTokenHeader, permissionsToken.value)) ~> + authorize(TestRoleAllowedByTokenPermission) { ctx => + complete(s"Alright, user ${ctx.authenticatedUser.id} is authorized by permissions token") } ~> check { handled shouldBe true - responseAs[String] shouldBe "Alright, user \"1\" is authorized" + responseAs[String] shouldBe "Alright, user 1 is authorized by permissions token" } } } -- cgit v1.2.3 From 4f11172ee721f7af12f4ff39cfa96a698fc88342 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Wed, 3 May 2017 18:16:38 -0700 Subject: Override hashCode equals and toString, rename to AuthorizedRequestContext --- src/main/scala/xyz/driver/core/rest.scala | 41 ++++++++++++++++++++------- src/test/scala/xyz/driver/core/AuthTest.scala | 15 +++++----- 2 files changed, 38 insertions(+), 18 deletions(-) (limited to 'src/main/scala/xyz/driver/core/rest.scala') diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index 4e965aa..bacb120 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -100,23 +100,42 @@ package rest { def permissionsToken: Option[PermissionsToken] = contextHeaders.get(AuthProvider.PermissionsTokenHeader).map(PermissionsToken.apply) - def withAuthenticatedUser[U <: User](authToken: AuthToken, user: U): AuthenticatedRequestContext[U] = - new AuthenticatedRequestContext(trackingId, + def withAuthenticatedUser[U <: User](authToken: AuthToken, user: U): AuthorizedRequestContext[U] = + new AuthorizedRequestContext(trackingId, contextHeaders.updated(AuthProvider.AuthenticationTokenHeader, authToken.value), user) + + override def hashCode(): Int = + Seq[Any](trackingId, contextHeaders).foldLeft(31)((result, obj) => 31 * result + obj.hashCode()) + + override def equals(obj: Any): Boolean = obj match { + case ctx: RequestContext => trackingId == ctx.trackingId && contextHeaders == ctx.contextHeaders + case _ => false + } + + override def toString: String = s"RequestContext($trackingId, $contextHeaders)" } - class AuthenticatedRequestContext[U <: User](override val trackingId: String = generators.nextUuid().toString, - override val contextHeaders: Map[String, String] = + class AuthorizedRequestContext[U <: User](override val trackingId: String = generators.nextUuid().toString, + override val contextHeaders: Map[String, String] = Map.empty[String, String], - val authenticatedUser: U) + val authenticatedUser: U) extends RequestContext { - def withPermissionsToken(permissionsToken: PermissionsToken): AuthenticatedRequestContext[U] = - new AuthenticatedRequestContext[U]( + def withPermissionsToken(permissionsToken: PermissionsToken): AuthorizedRequestContext[U] = + new AuthorizedRequestContext[U]( trackingId, contextHeaders.updated(AuthProvider.PermissionsTokenHeader, permissionsToken.value), authenticatedUser) + + override def hashCode(): Int = 31 * super.hashCode() + authenticatedUser.hashCode() + + override def equals(obj: Any): Boolean = obj match { + case ctx: AuthorizedRequestContext[U] => super.equals(ctx) && ctx.authenticatedUser == authenticatedUser + case _ => false + } + + override def toString: String = s"AuthenticatedRequestContext($trackingId, $contextHeaders, $authenticatedUser)" } object ContextHeaders { @@ -135,13 +154,13 @@ package rest { trait Authorization[U <: User] { def userHasPermissions(permissions: Seq[Permission])( - implicit ctx: AuthenticatedRequestContext[U]): OptionT[Future, + implicit ctx: AuthorizedRequestContext[U]): OptionT[Future, (Map[Permission, Boolean], PermissionsToken)] } class AlwaysAllowAuthorization[U <: User] extends Authorization[U] { override def userHasPermissions(permissions: Seq[Permission])( - implicit ctx: AuthenticatedRequestContext[U]): OptionT[Future, + implicit ctx: AuthorizedRequestContext[U]): OptionT[Future, (Map[Permission, Boolean], PermissionsToken)] = OptionT.optionT(Future.successful(Option((permissions.map(_ -> true).toMap, PermissionsToken(""))))) } @@ -165,7 +184,7 @@ package rest { /** * Verifies if request is authenticated and authorized to have `permissions` */ - def authorize(permissions: Permission*): Directive1[AuthenticatedRequestContext[U]] = { + def authorize(permissions: Permission*): Directive1[AuthorizedRequestContext[U]] = { serviceContext flatMap { ctx => onComplete { (for { @@ -194,7 +213,7 @@ package rest { } protected def userHasPermission(user: U, permissions: Seq[Permission])( - ctx: AuthenticatedRequestContext[U]): OptionT[Future, (Boolean, PermissionsToken)] = { + ctx: AuthorizedRequestContext[U]): OptionT[Future, (Boolean, PermissionsToken)] = { import spray.json._ def authorizedByToken: OptionT[Future, (Boolean, PermissionsToken)] = { diff --git a/src/test/scala/xyz/driver/core/AuthTest.scala b/src/test/scala/xyz/driver/core/AuthTest.scala index 441b9c8..9c86577 100644 --- a/src/test/scala/xyz/driver/core/AuthTest.scala +++ b/src/test/scala/xyz/driver/core/AuthTest.scala @@ -10,7 +10,7 @@ import org.scalatest.{FlatSpec, Matchers} import pdi.jwt.{Jwt, JwtAlgorithm} import xyz.driver.core.auth._ import xyz.driver.core.logging._ -import xyz.driver.core.rest.{AuthProvider, AuthenticatedRequestContext, Authorization, RequestContext} +import xyz.driver.core.rest.{AuthProvider, AuthorizedRequestContext, Authorization, RequestContext} import scala.concurrent.Future import scalaz.OptionT @@ -36,7 +36,7 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo val authorization: Authorization[User] = new Authorization[User] { override def userHasPermissions(permissions: Seq[Permission])( - implicit ctx: AuthenticatedRequestContext[User]): OptionT[Future, + implicit ctx: AuthorizedRequestContext[User]): OptionT[Future, (Map[Permission, Boolean], PermissionsToken)] = { val permissionsMap = permissions.map(p => p -> (p === TestRoleAllowedPermission)).toMap val token = PermissionsToken("TODO") @@ -107,11 +107,12 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo it should "authorize permission found in permissions token" in { import spray.json._ - val claim = JsObject(Map( - "iss" -> JsString("users"), - "sub" -> JsString("1"), - "permissions" -> JsObject(Map(TestRoleAllowedByTokenPermission.toString -> JsBoolean(true))) - )).prettyPrint + val claim = JsObject( + Map( + "iss" -> JsString("users"), + "sub" -> JsString("1"), + "permissions" -> JsObject(Map(TestRoleAllowedByTokenPermission.toString -> JsBoolean(true))) + )).prettyPrint val permissionsToken = PermissionsToken(Jwt.encode(claim, privateKey, JwtAlgorithm.RS256)) val referenceAuthToken = AuthToken("I am token") -- cgit v1.2.3 From 0e1d65445524b9819f701b67e11bebd03121964c Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Thu, 11 May 2017 16:13:10 -0700 Subject: Permissions token refactors --- src/main/scala/xyz/driver/core/rest.scala | 166 +++++++++++++++----------- src/test/scala/xyz/driver/core/AuthTest.scala | 25 ++-- 2 files changed, 110 insertions(+), 81 deletions(-) (limited to 'src/main/scala/xyz/driver/core/rest.scala') diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index bacb120..3f61246 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -27,13 +27,13 @@ import scalaz.{ListT, OptionT} package rest { object `package` { - import akka.http.scaladsl.server.{RequestContext => _, _} + import akka.http.scaladsl.server._ import Directives._ - def serviceContext: Directive1[RequestContext] = extract(ctx => extractServiceContext(ctx.request)) + def serviceContext: Directive1[ServiceRequestContext] = extract(ctx => extractServiceContext(ctx.request)) - def extractServiceContext(request: HttpRequest): RequestContext = - new RequestContext(extractTrackingId(request), extractContextHeaders(request)) + def extractServiceContext(request: HttpRequest): ServiceRequestContext = + new ServiceRequestContext(extractTrackingId(request), extractContextHeaders(request)) def extractTrackingId(request: HttpRequest): String = { request.headers @@ -92,38 +92,39 @@ package rest { } } - class RequestContext(val trackingId: String = generators.nextUuid().toString, - val contextHeaders: Map[String, String] = Map.empty[String, String]) { + class ServiceRequestContext(val trackingId: String = generators.nextUuid().toString, + val contextHeaders: Map[String, String] = Map.empty[String, String]) { def authToken: Option[AuthToken] = contextHeaders.get(AuthProvider.AuthenticationTokenHeader).map(AuthToken.apply) def permissionsToken: Option[PermissionsToken] = contextHeaders.get(AuthProvider.PermissionsTokenHeader).map(PermissionsToken.apply) - def withAuthenticatedUser[U <: User](authToken: AuthToken, user: U): AuthorizedRequestContext[U] = - new AuthorizedRequestContext(trackingId, - contextHeaders.updated(AuthProvider.AuthenticationTokenHeader, authToken.value), - user) + def withAuthenticatedUser[U <: User](authToken: AuthToken, user: U): AuthorizedServiceRequestContext[U] = + new AuthorizedServiceRequestContext( + trackingId, + contextHeaders.updated(AuthProvider.AuthenticationTokenHeader, authToken.value), + user) override def hashCode(): Int = Seq[Any](trackingId, contextHeaders).foldLeft(31)((result, obj) => 31 * result + obj.hashCode()) override def equals(obj: Any): Boolean = obj match { - case ctx: RequestContext => trackingId == ctx.trackingId && contextHeaders == ctx.contextHeaders - case _ => false + case ctx: ServiceRequestContext => trackingId === ctx.trackingId && contextHeaders === ctx.contextHeaders + case _ => false } override def toString: String = s"RequestContext($trackingId, $contextHeaders)" } - class AuthorizedRequestContext[U <: User](override val trackingId: String = generators.nextUuid().toString, - override val contextHeaders: Map[String, String] = - Map.empty[String, String], - val authenticatedUser: U) - extends RequestContext { + class AuthorizedServiceRequestContext[U <: User](override val trackingId: String = generators.nextUuid().toString, + override val contextHeaders: Map[String, String] = + Map.empty[String, String], + val authenticatedUser: U) + extends ServiceRequestContext { - def withPermissionsToken(permissionsToken: PermissionsToken): AuthorizedRequestContext[U] = - new AuthorizedRequestContext[U]( + def withPermissionsToken(permissionsToken: PermissionsToken): AuthorizedServiceRequestContext[U] = + new AuthorizedServiceRequestContext[U]( trackingId, contextHeaders.updated(AuthProvider.PermissionsTokenHeader, permissionsToken.value), authenticatedUser) @@ -131,8 +132,8 @@ package rest { override def hashCode(): Int = 31 * super.hashCode() + authenticatedUser.hashCode() override def equals(obj: Any): Boolean = obj match { - case ctx: AuthorizedRequestContext[U] => super.equals(ctx) && ctx.authenticatedUser == authenticatedUser - case _ => false + case ctx: AuthorizedServiceRequestContext[U] => super.equals(ctx) && ctx.authenticatedUser == authenticatedUser + case _ => false } override def toString: String = s"AuthenticatedRequestContext($trackingId, $contextHeaders, $authenticatedUser)" @@ -152,24 +153,79 @@ package rest { val SetPermissionsTokenHeader = "set-permissions" } + final case class AuthorizationResult(authorized: Boolean, token: Option[PermissionsToken]) + object AuthorizationResult { + val unauthorized: AuthorizationResult = AuthorizationResult(authorized = false, None) + } + trait Authorization[U <: User] { def userHasPermissions(permissions: Seq[Permission])( - implicit ctx: AuthorizedRequestContext[U]): OptionT[Future, - (Map[Permission, Boolean], PermissionsToken)] + implicit ctx: AuthorizedServiceRequestContext[U]): Future[AuthorizationResult] } - class AlwaysAllowAuthorization[U <: User] extends Authorization[U] { + class AlwaysAllowAuthorization[U <: User](implicit execution: ExecutionContext) extends Authorization[U] { + override def userHasPermissions(permissions: Seq[Permission])( + implicit ctx: AuthorizedServiceRequestContext[U]): Future[AuthorizationResult] = + Future.successful(AuthorizationResult(authorized = true, ctx.permissionsToken)) + } + + class CachedTokenAuthorization[U <: User](publicKey: PublicKey, issuer: String) extends Authorization[U] { + override def userHasPermissions(permissions: Seq[Permission])( + implicit ctx: AuthorizedServiceRequestContext[U]): Future[AuthorizationResult] = { + import spray.json._ + + val result = for { + token <- ctx.permissionsToken + jwt <- Jwt.decode(token.value, publicKey, Seq(JwtAlgorithm.RS256)).toOption + jwtJson = jwt.parseJson.asJsObject + + // Ensure jwt is for the currently authenticated user and the correct issuer, otherwise return None + _ <- jwtJson.fields.get("sub").contains(JsString(ctx.authenticatedUser.id.value)).option(()) + _ <- jwtJson.fields.get("iss").contains(JsString(issuer)).option(()) + + permissionsMap <- jwtJson.fields.get("permissions").collect { + case JsObject(fields) => + fields.collect { + case (key, JsBoolean(value)) => key -> value + } + } + + authorized = permissions.forall(p => permissionsMap.get(p.toString).contains(true)) + } yield AuthorizationResult(authorized, Some(token)) + + Future.successful(result.getOrElse(AuthorizationResult.unauthorized)) + } + } + + class ChainedAuthorization[U <: User](authorizations: Authorization[U]*)(implicit execution: ExecutionContext) + extends Authorization[U] { + override def userHasPermissions(permissions: Seq[Permission])( - implicit ctx: AuthorizedRequestContext[U]): OptionT[Future, - (Map[Permission, Boolean], PermissionsToken)] = - OptionT.optionT(Future.successful(Option((permissions.map(_ -> true).toMap, PermissionsToken(""))))) + implicit ctx: AuthorizedServiceRequestContext[U]): Future[AuthorizationResult] = { + def callAuthorizations( + remainingAuthorizations: List[Authorization[U]] = authorizations.toList): Future[AuthorizationResult] = { + remainingAuthorizations match { + case auth :: Nil => auth.userHasPermissions(permissions) + case auth :: rest => + auth + .userHasPermissions(permissions) + .flatMap( + result => + if (result.authorized) Future.successful(result) + else callAuthorizations(rest)) + case Nil => Future.successful(AuthorizationResult.unauthorized) + } + } + + callAuthorizations() + } + } - abstract class AuthProvider[U <: User](val authorization: Authorization[U], - val permissionsTokenPublicKey: PublicKey, - log: Logger)(implicit execution: ExecutionContext) { + abstract class AuthProvider[U <: User](val authorization: Authorization[U], log: Logger)( + implicit execution: ExecutionContext) { - import akka.http.scaladsl.server.{RequestContext => _, _} + import akka.http.scaladsl.server._ import Directives._ /** @@ -179,20 +235,21 @@ package rest { * @param ctx set of request values which can be relevant to authenticate user * @return authenticated user */ - def authenticatedUser(implicit ctx: RequestContext): OptionT[Future, U] + def authenticatedUser(implicit ctx: ServiceRequestContext): OptionT[Future, U] /** * Verifies if request is authenticated and authorized to have `permissions` */ - def authorize(permissions: Permission*): Directive1[AuthorizedRequestContext[U]] = { + def authorize(permissions: Permission*): Directive1[AuthorizedServiceRequestContext[U]] = { serviceContext flatMap { ctx => onComplete { (for { authToken <- OptionT.optionT(Future.successful(ctx.authToken)) user <- authenticatedUser(ctx) authCtx = ctx.withAuthenticatedUser(authToken, user) - (authorizationResult, permissionsToken) <- userHasPermission(user, permissions)(authCtx) - } yield (authCtx.withPermissionsToken(permissionsToken), authorizationResult)).run + authorizationResult <- authorization.userHasPermissions(permissions)(authCtx).toOptionT + cachedPermissionsAuthCtx = authorizationResult.token.fold(authCtx)(authCtx.withPermissionsToken) + } yield (cachedPermissionsAuthCtx, authorizationResult.authorized)).run } flatMap { case Success(Some((authCtx, true))) => provide(authCtx) case Success(Some((authCtx, false))) => @@ -211,37 +268,6 @@ package rest { } } } - - protected def userHasPermission(user: U, permissions: Seq[Permission])( - ctx: AuthorizedRequestContext[U]): OptionT[Future, (Boolean, PermissionsToken)] = { - import spray.json._ - - def authorizedByToken: OptionT[Future, (Boolean, PermissionsToken)] = { - OptionT.optionT(Future.successful(for { - token <- ctx.permissionsToken - jwt <- Jwt.decode(token.value, permissionsTokenPublicKey, Seq(JwtAlgorithm.RS256)).toOption - jwtJson = jwt.parseJson.asJsObject - - // Ensure jwt is for the currently authenticated user, otherwise return None to call permissions service - _ <- jwtJson.fields.get("sub").contains(JsString(user.id.value)).option(()) - - permissionsMap <- jwtJson.fields.get("permissions").map(_.asJsObject.fields) - - // Ensure all permissions are in the token, otherwise return none to call permissions service - _ <- permissions.forall(p => permissionsMap.contains(p.toString)).option(()) - - authorized = permissions.forall(p => permissionsMap.get(p.toString).contains(JsBoolean(true))) - } yield (authorized, token))) - } - - def authorizedByService: OptionT[Future, (Boolean, PermissionsToken)] = - authorization.userHasPermissions(permissions)(ctx).map { - case (permissionMap, token) => - (permissions.forall(p => permissionMap.getOrElse(p, false)), token) - } - - authorizedByToken.orElse(authorizedByService) - } } trait Service @@ -294,9 +320,9 @@ package rest { trait ServiceTransport { - def sendRequestGetResponse(context: RequestContext)(requestStub: HttpRequest): Future[HttpResponse] + def sendRequestGetResponse(context: ServiceRequestContext)(requestStub: HttpRequest): Future[HttpResponse] - def sendRequest(context: RequestContext)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] + def sendRequest(context: ServiceRequestContext)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] } trait ServiceDiscovery { @@ -313,7 +339,7 @@ package rest { protected implicit val materializer = ActorMaterializer()(actorSystem) protected implicit val execution = executionContext - def sendRequestGetResponse(context: RequestContext)(requestStub: HttpRequest): Future[HttpResponse] = { + def sendRequestGetResponse(context: ServiceRequestContext)(requestStub: HttpRequest): Future[HttpResponse] = { val requestTime = time.currentTime() @@ -341,7 +367,7 @@ package rest { response } - def sendRequest(context: RequestContext)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] = { + def sendRequest(context: ServiceRequestContext)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] = { sendRequestGetResponse(context)(requestStub) map { response => if (response.status == StatusCodes.NotFound) { diff --git a/src/test/scala/xyz/driver/core/AuthTest.scala b/src/test/scala/xyz/driver/core/AuthTest.scala index 9c86577..8de0e87 100644 --- a/src/test/scala/xyz/driver/core/AuthTest.scala +++ b/src/test/scala/xyz/driver/core/AuthTest.scala @@ -3,14 +3,14 @@ package xyz.driver.core import akka.http.scaladsl.model.headers.{HttpChallenges, RawHeader} import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.{RequestContext => _, _} +import akka.http.scaladsl.server._ import akka.http.scaladsl.testkit.ScalatestRouteTest import org.scalatest.mock.MockitoSugar import org.scalatest.{FlatSpec, Matchers} import pdi.jwt.{Jwt, JwtAlgorithm} import xyz.driver.core.auth._ import xyz.driver.core.logging._ -import xyz.driver.core.rest.{AuthProvider, AuthorizedRequestContext, Authorization, RequestContext} +import xyz.driver.core.rest._ import scala.concurrent.Future import scalaz.OptionT @@ -33,19 +33,22 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo (keyPair.getPublic, keyPair.getPrivate) } - val authorization: Authorization[User] = new Authorization[User] { + val basicAuthorization: Authorization[User] = new Authorization[User] { override def userHasPermissions(permissions: Seq[Permission])( - implicit ctx: AuthorizedRequestContext[User]): OptionT[Future, - (Map[Permission, Boolean], PermissionsToken)] = { - val permissionsMap = permissions.map(p => p -> (p === TestRoleAllowedPermission)).toMap - val token = PermissionsToken("TODO") - OptionT.optionT(Future.successful(Option((permissionsMap, token)))) + implicit ctx: AuthorizedServiceRequestContext[User]): Future[AuthorizationResult] = { + val authorized = permissions.forall(_ === TestRoleAllowedPermission) + Future.successful(AuthorizationResult(authorized, ctx.permissionsToken)) } } - val authStatusService = new AuthProvider[User](authorization, publicKey, NoLogger) { - override def authenticatedUser(implicit ctx: RequestContext): OptionT[Future, User] = + val tokenIssuer = "users" + val tokenAuthorization = new CachedTokenAuthorization[User](publicKey, tokenIssuer) + + val authorization = new ChainedAuthorization[User](tokenAuthorization, basicAuthorization) + + val authStatusService = new AuthProvider[User](authorization, NoLogger) { + override def authenticatedUser(implicit ctx: ServiceRequestContext): OptionT[Future, User] = OptionT.optionT[Future] { if (ctx.contextHeaders.keySet.contains(AuthProvider.AuthenticationTokenHeader)) { Future.successful(Some(BasicUser(Id[User]("1"), Set(TestRole)))) @@ -109,7 +112,7 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo val claim = JsObject( Map( - "iss" -> JsString("users"), + "iss" -> JsString(tokenIssuer), "sub" -> JsString("1"), "permissions" -> JsObject(Map(TestRoleAllowedByTokenPermission.toString -> JsBoolean(true))) )).prettyPrint -- cgit v1.2.3 From a81fba794fb41570edaea77ad47d3b96e2e484ec Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Thu, 11 May 2017 16:16:31 -0700 Subject: Change signature of RestService#get --- src/main/scala/xyz/driver/core/rest.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src/main/scala/xyz/driver/core/rest.scala') diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index 3f61246..1f8ddc2 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -299,8 +299,8 @@ package rest { protected def jsonEntity(json: JsValue): RequestEntity = HttpEntity(ContentTypes.`application/json`, json.compactPrint) - protected def get(baseUri: Uri, path: String, query: (String, String)*) = - HttpRequest(HttpMethods.GET, endpointUri(baseUri, path, query: _*)) + protected def get(baseUri: Uri, path: String, query: Seq[(String, String)]) = + HttpRequest(HttpMethods.GET, endpointUri(baseUri, path, query)) protected def post(baseUri: Uri, path: String, httpEntity: RequestEntity) = HttpRequest(HttpMethods.POST, endpointUri(baseUri, path), entity = httpEntity) @@ -314,7 +314,7 @@ package rest { protected def endpointUri(baseUri: Uri, path: String) = baseUri.withPath(Uri.Path(path)) - protected def endpointUri(baseUri: Uri, path: String, query: (String, String)*) = + protected def endpointUri(baseUri: Uri, path: String, query: Seq[(String, String)]) = baseUri.withPath(Uri.Path(path)).withQuery(Uri.Query(query: _*)) } -- cgit v1.2.3 From 59988c13988cece3a2a25db4cc823eb5cbdfc55b Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Thu, 11 May 2017 17:51:22 -0700 Subject: Add default for query parameter --- src/main/scala/xyz/driver/core/rest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/main/scala/xyz/driver/core/rest.scala') diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index 1f8ddc2..2c169ed 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -299,7 +299,7 @@ package rest { protected def jsonEntity(json: JsValue): RequestEntity = HttpEntity(ContentTypes.`application/json`, json.compactPrint) - protected def get(baseUri: Uri, path: String, query: Seq[(String, String)]) = + protected def get(baseUri: Uri, path: String, query: Seq[(String, String)] = Seq.empty) = HttpRequest(HttpMethods.GET, endpointUri(baseUri, path, query)) protected def post(baseUri: Uri, path: String, httpEntity: RequestEntity) = -- cgit v1.2.3 From 4b61ef72c29bc04680c05c3fbbd6952398ea2b7e Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Thu, 11 May 2017 18:54:05 -0700 Subject: Respond to a few more PR comments --- src/main/scala/xyz/driver/core/rest.scala | 40 +++++++++++++------------------ 1 file changed, 16 insertions(+), 24 deletions(-) (limited to 'src/main/scala/xyz/driver/core/rest.scala') diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index 2c169ed..55755ba 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -114,7 +114,7 @@ package rest { case _ => false } - override def toString: String = s"RequestContext($trackingId, $contextHeaders)" + override def toString: String = s"ServiceRequestContext($trackingId, $contextHeaders)" } class AuthorizedServiceRequestContext[U <: User](override val trackingId: String = generators.nextUuid().toString, @@ -136,7 +136,8 @@ package rest { case _ => false } - override def toString: String = s"AuthenticatedRequestContext($trackingId, $contextHeaders, $authenticatedUser)" + override def toString: String = + s"AuthorizedServiceRequestContext($trackingId, $contextHeaders, $authenticatedUser)" } object ContextHeaders { @@ -174,6 +175,14 @@ package rest { implicit ctx: AuthorizedServiceRequestContext[U]): Future[AuthorizationResult] = { import spray.json._ + def extractPermissionsFromTokenJSON(tokenObject: JsObject): Option[Map[String, Boolean]] = + tokenObject.fields.get("permissions").collect { + case JsObject(fields) => + fields.collect { + case (key, JsBoolean(value)) => key -> value + } + } + val result = for { token <- ctx.permissionsToken jwt <- Jwt.decode(token.value, publicKey, Seq(JwtAlgorithm.RS256)).toOption @@ -183,12 +192,7 @@ package rest { _ <- jwtJson.fields.get("sub").contains(JsString(ctx.authenticatedUser.id.value)).option(()) _ <- jwtJson.fields.get("iss").contains(JsString(issuer)).option(()) - permissionsMap <- jwtJson.fields.get("permissions").collect { - case JsObject(fields) => - fields.collect { - case (key, JsBoolean(value)) => key -> value - } - } + permissionsMap <- extractPermissionsFromTokenJSON(jwtJson) authorized = permissions.forall(p => permissionsMap.get(p.toString).contains(true)) } yield AuthorizationResult(authorized, Some(token)) @@ -202,24 +206,12 @@ package rest { override def userHasPermissions(permissions: Seq[Permission])( implicit ctx: AuthorizedServiceRequestContext[U]): Future[AuthorizationResult] = { - def callAuthorizations( - remainingAuthorizations: List[Authorization[U]] = authorizations.toList): Future[AuthorizationResult] = { - remainingAuthorizations match { - case auth :: Nil => auth.userHasPermissions(permissions) - case auth :: rest => - auth - .userHasPermissions(permissions) - .flatMap( - result => - if (result.authorized) Future.successful(result) - else callAuthorizations(rest)) - case Nil => Future.successful(AuthorizationResult.unauthorized) - } + authorizations.toList.foldLeftM[Future, AuthorizationResult](AuthorizationResult.unauthorized) { + (authResult, authorization) => + if (authResult.authorized) Future.successful(authResult) + else authorization.userHasPermissions(permissions) } - - callAuthorizations() } - } abstract class AuthProvider[U <: User](val authorization: Authorization[U], log: Logger)( -- cgit v1.2.3 From 2fc1c3baeef3662258caa64068fbcb25401a4065 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Fri, 12 May 2017 16:01:44 -0700 Subject: Re-add withAuthToken method to ServiceRequestToken --- src/main/scala/xyz/driver/core/rest.scala | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src/main/scala/xyz/driver/core/rest.scala') diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index 55755ba..5c4b332 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -100,11 +100,18 @@ package rest { def permissionsToken: Option[PermissionsToken] = contextHeaders.get(AuthProvider.PermissionsTokenHeader).map(PermissionsToken.apply) + def withAuthToken(authToken: AuthToken): ServiceRequestContext = + new ServiceRequestContext( + trackingId, + contextHeaders.updated(AuthProvider.AuthenticationTokenHeader, authToken.value) + ) + def withAuthenticatedUser[U <: User](authToken: AuthToken, user: U): AuthorizedServiceRequestContext[U] = new AuthorizedServiceRequestContext( trackingId, contextHeaders.updated(AuthProvider.AuthenticationTokenHeader, authToken.value), - user) + user + ) override def hashCode(): Int = Seq[Any](trackingId, contextHeaders).foldLeft(31)((result, obj) => 31 * result + obj.hashCode()) -- cgit v1.2.3 From 96735d492f5c0aebbc5d26d971c2c37514a26546 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Sun, 14 May 2017 21:10:56 -0600 Subject: Add user parameter to authorization method --- src/main/scala/xyz/driver/core/rest.scala | 22 +++++++++++----------- src/test/scala/xyz/driver/core/AuthTest.scala | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) (limited to 'src/main/scala/xyz/driver/core/rest.scala') diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index 5c4b332..0574916 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -167,19 +167,19 @@ package rest { } trait Authorization[U <: User] { - def userHasPermissions(permissions: Seq[Permission])( - implicit ctx: AuthorizedServiceRequestContext[U]): Future[AuthorizationResult] + def userHasPermissions(user: U, permissions: Seq[Permission])( + implicit ctx: ServiceRequestContext): Future[AuthorizationResult] } class AlwaysAllowAuthorization[U <: User](implicit execution: ExecutionContext) extends Authorization[U] { - override def userHasPermissions(permissions: Seq[Permission])( - implicit ctx: AuthorizedServiceRequestContext[U]): Future[AuthorizationResult] = + override def userHasPermissions(user: U, permissions: Seq[Permission])( + implicit ctx: ServiceRequestContext): Future[AuthorizationResult] = Future.successful(AuthorizationResult(authorized = true, ctx.permissionsToken)) } class CachedTokenAuthorization[U <: User](publicKey: PublicKey, issuer: String) extends Authorization[U] { - override def userHasPermissions(permissions: Seq[Permission])( - implicit ctx: AuthorizedServiceRequestContext[U]): Future[AuthorizationResult] = { + override def userHasPermissions(user: U, permissions: Seq[Permission])( + implicit ctx: ServiceRequestContext): Future[AuthorizationResult] = { import spray.json._ def extractPermissionsFromTokenJSON(tokenObject: JsObject): Option[Map[String, Boolean]] = @@ -196,7 +196,7 @@ package rest { jwtJson = jwt.parseJson.asJsObject // Ensure jwt is for the currently authenticated user and the correct issuer, otherwise return None - _ <- jwtJson.fields.get("sub").contains(JsString(ctx.authenticatedUser.id.value)).option(()) + _ <- jwtJson.fields.get("sub").contains(JsString(user.id.value)).option(()) _ <- jwtJson.fields.get("iss").contains(JsString(issuer)).option(()) permissionsMap <- extractPermissionsFromTokenJSON(jwtJson) @@ -211,12 +211,12 @@ package rest { class ChainedAuthorization[U <: User](authorizations: Authorization[U]*)(implicit execution: ExecutionContext) extends Authorization[U] { - override def userHasPermissions(permissions: Seq[Permission])( - implicit ctx: AuthorizedServiceRequestContext[U]): Future[AuthorizationResult] = { + override def userHasPermissions(user: U, permissions: Seq[Permission])( + implicit ctx: ServiceRequestContext): Future[AuthorizationResult] = { authorizations.toList.foldLeftM[Future, AuthorizationResult](AuthorizationResult.unauthorized) { (authResult, authorization) => if (authResult.authorized) Future.successful(authResult) - else authorization.userHasPermissions(permissions) + else authorization.userHasPermissions(user, permissions) } } } @@ -246,7 +246,7 @@ package rest { authToken <- OptionT.optionT(Future.successful(ctx.authToken)) user <- authenticatedUser(ctx) authCtx = ctx.withAuthenticatedUser(authToken, user) - authorizationResult <- authorization.userHasPermissions(permissions)(authCtx).toOptionT + authorizationResult <- authorization.userHasPermissions(user, permissions)(authCtx).toOptionT cachedPermissionsAuthCtx = authorizationResult.token.fold(authCtx)(authCtx.withPermissionsToken) } yield (cachedPermissionsAuthCtx, authorizationResult.authorized)).run } flatMap { diff --git a/src/test/scala/xyz/driver/core/AuthTest.scala b/src/test/scala/xyz/driver/core/AuthTest.scala index 8de0e87..bf776df 100644 --- a/src/test/scala/xyz/driver/core/AuthTest.scala +++ b/src/test/scala/xyz/driver/core/AuthTest.scala @@ -35,8 +35,8 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo val basicAuthorization: Authorization[User] = new Authorization[User] { - override def userHasPermissions(permissions: Seq[Permission])( - implicit ctx: AuthorizedServiceRequestContext[User]): Future[AuthorizationResult] = { + override def userHasPermissions(user: User, permissions: Seq[Permission])( + implicit ctx: ServiceRequestContext): Future[AuthorizationResult] = { val authorized = permissions.forall(_ === TestRoleAllowedPermission) Future.successful(AuthorizationResult(authorized, ctx.permissionsToken)) } -- cgit v1.2.3 From 763e68cb1140ed8b1d0bbbeccec45debce3a66b1 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Tue, 16 May 2017 19:05:13 -0700 Subject: Add apply method to CachedTokenAuthorization to pass in publicKeyFile param --- src/main/scala/xyz/driver/core/rest.scala | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'src/main/scala/xyz/driver/core/rest.scala') diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index 0574916..2ff8ae8 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -1,5 +1,9 @@ package xyz.driver.core +import java.nio.file.{Files, Path} +import java.security.spec.X509EncodedKeySpec +import java.security.{KeyFactory, PublicKey} + import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.http.scaladsl.model._ @@ -14,7 +18,6 @@ import com.github.swagger.akka.{HasActorSystem, SwaggerHttpService} import com.typesafe.config.Config import com.typesafe.scalalogging.Logger import io.swagger.models.Scheme -import java.security.PublicKey import pdi.jwt.{Jwt, JwtAlgorithm} import xyz.driver.core.auth._ import xyz.driver.core.time.provider.TimeProvider @@ -208,6 +211,17 @@ package rest { } } + object CachedTokenAuthorization { + def apply[U <: User](publicKeyFile: Path, issuer: String): CachedTokenAuthorization[U] = { + val publicKey: PublicKey = { + val publicKeyBytes = Files.readAllBytes(publicKeyFile) + val spec = new X509EncodedKeySpec(publicKeyBytes) + KeyFactory.getInstance("RSA").generatePublic(spec) + } + new CachedTokenAuthorization[U](publicKey, issuer) + } + } + class ChainedAuthorization[U <: User](authorizations: Authorization[U]*)(implicit execution: ExecutionContext) extends Authorization[U] { -- cgit v1.2.3 From 5d3de80a62fcf4cebec7d87cc3ef503d40c30b5a Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Wed, 24 May 2017 17:10:24 -0700 Subject: Change to lazy loading public key --- src/main/scala/xyz/driver/core/file/S3Storage.scala | 12 +++++++----- src/main/scala/xyz/driver/core/rest.scala | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) (limited to 'src/main/scala/xyz/driver/core/rest.scala') diff --git a/src/main/scala/xyz/driver/core/file/S3Storage.scala b/src/main/scala/xyz/driver/core/file/S3Storage.scala index 50bfe85..933b01a 100644 --- a/src/main/scala/xyz/driver/core/file/S3Storage.scala +++ b/src/main/scala/xyz/driver/core/file/S3Storage.scala @@ -53,11 +53,13 @@ class S3Storage(s3: AmazonS3, bucket: Name[Bucket], executionContext: ExecutionC result.isTruncated } flatMap { result => result.getObjectSummaries.asScala.toList.map { summary => - FileLink(Name[File](summary.getKey), - Paths.get(path.toString + "/" + summary.getKey), - Revision[File](summary.getETag), - Time(summary.getLastModified.getTime), - summary.getSize) + FileLink( + Name[File](summary.getKey), + Paths.get(path.toString + "/" + summary.getKey), + Revision[File](summary.getETag), + Time(summary.getLastModified.getTime), + summary.getSize + ) } filterNot isInSubFolder(path) } toList }) diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index 2ff8ae8..1db9d09 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -180,7 +180,7 @@ package rest { Future.successful(AuthorizationResult(authorized = true, ctx.permissionsToken)) } - class CachedTokenAuthorization[U <: User](publicKey: PublicKey, issuer: String) extends Authorization[U] { + class CachedTokenAuthorization[U <: User](publicKey: => PublicKey, issuer: String) extends Authorization[U] { override def userHasPermissions(user: U, permissions: Seq[Permission])( implicit ctx: ServiceRequestContext): Future[AuthorizationResult] = { import spray.json._ @@ -213,7 +213,7 @@ package rest { object CachedTokenAuthorization { def apply[U <: User](publicKeyFile: Path, issuer: String): CachedTokenAuthorization[U] = { - val publicKey: PublicKey = { + lazy val publicKey: PublicKey = { val publicKeyBytes = Files.readAllBytes(publicKeyFile) val spec = new X509EncodedKeySpec(publicKeyBytes) KeyFactory.getInstance("RSA").generatePublic(spec) -- cgit v1.2.3