From 94c324e249ab3e5848191602c3549cef350e83b1 Mon Sep 17 00:00:00 2001 From: vlad Date: Mon, 3 Oct 2016 14:25:24 -0700 Subject: Auth service providing directive and using external source of token-to-user resolution --- README.md | 2 +- src/main/scala/com/drivergrp/core/auth.scala | 68 +++++++++++++++--------- src/main/scala/com/drivergrp/core/rest.scala | 4 +- src/test/scala/com/drivergrp/core/AuthTest.scala | 35 ++++++++---- 4 files changed, 73 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 25ada84..2bc5820 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ For more examples check [project tests](https://github.com/drivergroup/driver-co $ sbt publish-local -3. TODO: Release new version of core: +3. Release new version of core: $ sbt release diff --git a/src/main/scala/com/drivergrp/core/auth.scala b/src/main/scala/com/drivergrp/core/auth.scala index 2d61cbf..6b8cdaa 100644 --- a/src/main/scala/com/drivergrp/core/auth.scala +++ b/src/main/scala/com/drivergrp/core/auth.scala @@ -3,6 +3,10 @@ package com.drivergrp.core import akka.http.scaladsl.model.headers.HttpChallenges import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected +import scala.concurrent.Future +import scala.util.{Failure, Success, Try} +import scalaz.OptionT + object auth { sealed trait Permission @@ -79,39 +83,55 @@ object auth { final case class PasswordHash(value: String) - def extractUser(authToken: AuthToken): User = { - new User() { - override def id: Id[User] = Id[User](1L) - override def roles: Set[Role] = Set(PathologistRole) - } - - // TODO: or reject(ValidationRejection(s"Wasn't able to extract user for the token provided")) if none + object AuthService { + val AuthenticationTokenHeader = "WWW-Authenticate" } - object directives { + trait AuthService[U <: User] { + import akka.http.scaladsl.server._ import Directives._ - val AuthenticationTokenHeader = "WWW-Authenticate" + protected def authStatus(authToken: AuthToken): OptionT[Future, U] - def authorize(permission: Permission): Directive1[AuthToken] = { + def authorize(permission: Permission): Directive1[(AuthToken, U)] = { parameters('authToken.?).flatMap { parameterTokenValue => - optionalHeaderValueByName(AuthenticationTokenHeader).flatMap { headerTokenValue => - headerTokenValue.orElse(parameterTokenValue) match { - case Some(tokenValue) => - val token = AuthToken(Base64[Macaroon](tokenValue)) - - if (extractUser(token).roles.exists(_.hasPermission(permission))) provide(token) - else { - val challenge = HttpChallenges.basic(s"User does not have the required permission $permission") - reject(AuthenticationFailedRejection(CredentialsRejected, challenge)) - } - - case None => - reject(MissingHeaderRejection("WWW-Authenticate")) - } + optionalHeaderValueByName(AuthService.AuthenticationTokenHeader).flatMap { headerTokenValue => + verifyAuthToken(headerTokenValue.orElse(parameterTokenValue), permission) } } } + + private def verifyAuthToken(tokenOption: Option[String], permission: Permission): Directive1[(AuthToken, U)] = + tokenOption match { + case Some(tokenValue) => + val token = AuthToken(Base64[Macaroon](tokenValue)) + + onComplete(authStatus(token).run).flatMap { tokenUserResult => + checkPermissions(tokenUserResult, permission, token) + } + + case None => + reject(MissingHeaderRejection(AuthService.AuthenticationTokenHeader)) + } + + private def checkPermissions(userResult: Try[Option[U]], + permission: Permission, + token: AuthToken): Directive1[(AuthToken, U)] = { + userResult match { + case Success(Some(user)) => + if (user.roles.exists(_.hasPermission(permission))) provide(token -> user) + else { + val challenge = HttpChallenges.basic(s"User does not have the required permission $permission") + reject(AuthenticationFailedRejection(CredentialsRejected, challenge)) + } + + case Success(None) => + reject(ValidationRejection(s"Wasn't able to find authenticated user for the token provided")) + + case Failure(t) => + reject(ValidationRejection(s"Wasn't able to verify token for authenticated user", Some(t))) + } + } } } diff --git a/src/main/scala/com/drivergrp/core/rest.scala b/src/main/scala/com/drivergrp/core/rest.scala index 0d718c9..d97e13e 100644 --- a/src/main/scala/com/drivergrp/core/rest.scala +++ b/src/main/scala/com/drivergrp/core/rest.scala @@ -8,7 +8,7 @@ import akka.http.scaladsl.unmarshalling.Unmarshal import akka.stream.ActorMaterializer import akka.stream.scaladsl.Flow import akka.util.ByteString -import com.drivergrp.core.auth.AuthToken +import com.drivergrp.core.auth.{AuthService, AuthToken} import com.drivergrp.core.crypto.Crypto import com.drivergrp.core.logging.Logger import com.drivergrp.core.stats.Stats @@ -55,7 +55,7 @@ object rest { val request = requestStub .withEntity(requestStub.entity.transformDataBytes(encryptionFlow)) .withHeaders( - RawHeader(auth.directives.AuthenticationTokenHeader, s"Macaroon ${authToken.value.value}")) + RawHeader(AuthService.AuthenticationTokenHeader, authToken.value.value)) log.audit(s"Sending to ${request.uri} request $request") diff --git a/src/test/scala/com/drivergrp/core/AuthTest.scala b/src/test/scala/com/drivergrp/core/AuthTest.scala index 992ae83..42f9155 100644 --- a/src/test/scala/com/drivergrp/core/AuthTest.scala +++ b/src/test/scala/com/drivergrp/core/AuthTest.scala @@ -9,13 +9,28 @@ import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsReject import org.scalatest.mock.MockitoSugar import org.scalatest.{FlatSpec, Matchers} +import scala.concurrent.Future +import scalaz.OptionT + class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRouteTest { + val authStatusService: AuthService[User] = new AuthService[User] { + override def authStatus(authToken: AuthToken): OptionT[Future, User] = OptionT.optionT[Future] { + Future.successful(Some(new User() { + override def id: Id[User] = Id[User](1L) + override def roles: Set[Role] = Set(PathologistRole) + })) + } + } + + import authStatusService._ + "'authorize' directive" should "throw error is auth token is not in the request" in { Get("/naive/attempt") ~> - auth.directives.authorize(CanSignOutReport) { authToken => - complete("Never going to be here") + authorize(CanSignOutReport) { + case (authToken, user) => + complete("Never going to be here") } ~> check { handled shouldBe false @@ -28,10 +43,11 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo val referenceAuthToken = AuthToken(Base64("I am a pathologist's token")) Post("/administration/attempt").addHeader( - RawHeader(auth.directives.AuthenticationTokenHeader, s"Macaroon ${referenceAuthToken.value.value}") + RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value.value) ) ~> - auth.directives.authorize(CanAssignRoles) { authToken => - complete("Never going to get here") + authorize(CanAssignRoles) { + case (authToken, user) => + complete("Never going to get here") } ~> check { handled shouldBe false @@ -47,14 +63,15 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo val referenceAuthToken = AuthToken(Base64("I am token")) Get("/valid/attempt/?a=2&b=5").addHeader( - RawHeader(auth.directives.AuthenticationTokenHeader, s"Macaroon ${referenceAuthToken.value.value}") + RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value.value) ) ~> - auth.directives.authorize(CanSignOutReport) { authToken => - complete("Alright, \"" + authToken.value.value + "\" is handled") + authorize(CanSignOutReport) { + case (authToken, user) => + complete("Alright, \"" + authToken.value.value + "\" is handled") } ~> check { handled shouldBe true - responseAs[String] shouldBe "Alright, \"Macaroon I am token\" is handled" + responseAs[String] shouldBe "Alright, \"I am token\" is handled" } } } -- cgit v1.2.3