From 51f7f400cab116c2511df1d27ca4edfc3cb0cbc3 Mon Sep 17 00:00:00 2001 From: vlad Date: Thu, 2 Feb 2017 18:52:29 -0500 Subject: Removing permissions from user and role as permission is something to check not something to have now Adding couple handy case classes --- src/main/scala/xyz/driver/core/auth.scala | 20 ++++++ src/main/scala/xyz/driver/core/core.scala | 2 + src/main/scala/xyz/driver/core/generators.scala | 2 +- src/main/scala/xyz/driver/core/json.scala | 9 +++ src/main/scala/xyz/driver/core/rest.scala | 96 +++++++++++-------------- src/test/scala/xyz/driver/core/AuthTest.scala | 22 +++--- 6 files changed, 87 insertions(+), 64 deletions(-) create mode 100644 src/main/scala/xyz/driver/core/auth.scala (limited to 'src') diff --git a/src/main/scala/xyz/driver/core/auth.scala b/src/main/scala/xyz/driver/core/auth.scala new file mode 100644 index 0000000..a9f52e5 --- /dev/null +++ b/src/main/scala/xyz/driver/core/auth.scala @@ -0,0 +1,20 @@ +package xyz.driver.core + +object auth { + + trait Permission + + final case class Role(id: Id[Role], name: Name[Role]) + + trait User { + def id: Id[User] + def roles: Set[Role] + } + + final case class BasicUser(id: Id[User], roles: Set[Role]) extends User + + final case class AuthToken(value: String) + final case class RefreshToken(value: String) + + final case class PasswordHash(value: String) +} diff --git a/src/main/scala/xyz/driver/core/core.scala b/src/main/scala/xyz/driver/core/core.scala index db2af95..341d991 100644 --- a/src/main/scala/xyz/driver/core/core.scala +++ b/src/main/scala/xyz/driver/core/core.scala @@ -65,4 +65,6 @@ package core { implicit def revisionEqual[T]: Equal[Revision[T]] = Equal.equal[Revision[T]](_.id == _.id) } + + final case class Base64(value: String) } diff --git a/src/main/scala/xyz/driver/core/generators.scala b/src/main/scala/xyz/driver/core/generators.scala index d532ae3..45c39fc 100644 --- a/src/main/scala/xyz/driver/core/generators.scala +++ b/src/main/scala/xyz/driver/core/generators.scala @@ -56,7 +56,7 @@ object generators { Time(scala.math.max(oneTime.millis, anotherTime.millis))) } - def nextDate(): Date = nextTime.toDate(java.util.TimeZone.getTimeZone("UTC")) + def nextDate(): Date = nextTime().toDate(java.util.TimeZone.getTimeZone("UTC")) def nextBigDecimal(multiplier: Double = 1000000.00, precision: Int = 2): BigDecimal = BigDecimal(multiplier * nextDouble, new MathContext(precision)) diff --git a/src/main/scala/xyz/driver/core/json.scala b/src/main/scala/xyz/driver/core/json.scala index 039f650..a10ab5f 100644 --- a/src/main/scala/xyz/driver/core/json.scala +++ b/src/main/scala/xyz/driver/core/json.scala @@ -96,6 +96,15 @@ object json { } } + implicit val base64Format = new RootJsonFormat[Base64] { + def write(base64Value: Base64) = JsString(base64Value.value) + + def read(value: JsValue): Base64 = value match { + case JsString(base64Value) => Base64(base64Value) + case _ => throw DeserializationException("Base64 format expects string") + } + } + class EnumJsonFormat[T](mapping: (String, T)*) extends RootJsonFormat[T] { private val map = mapping.toMap diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala index 437df3c..af23cd5 100644 --- a/src/main/scala/xyz/driver/core/rest.scala +++ b/src/main/scala/xyz/driver/core/rest.scala @@ -11,6 +11,7 @@ import com.github.swagger.akka.model._ import com.github.swagger.akka.{HasActorSystem, SwaggerHttpService} import com.typesafe.config.Config import io.swagger.models.Scheme +import xyz.driver.core.auth._ import xyz.driver.core.logging.Logger import xyz.driver.core.stats.Stats import xyz.driver.core.time.TimeRange @@ -27,8 +28,8 @@ object rest { trackingId: String = generators.nextUuid().toString, contextHeaders: Map[String, String] = Map.empty[String, String]) { - def authToken: Option[Auth.AuthToken] = - contextHeaders.get(Auth.AuthProvider.AuthenticationTokenHeader).map(Auth.AuthToken.apply) + def authToken: Option[AuthToken] = + contextHeaders.get(AuthProvider.AuthenticationTokenHeader).map(AuthToken.apply) } object ServiceRequestContext { @@ -67,66 +68,53 @@ object rest { } } - object Auth { - - trait Permission - - trait Role { - val id: Id[Role] - val name: Name[Role] - val permissions: Set[Permission] - - def hasPermission(permission: Permission): Boolean = permissions.contains(permission) + object AuthProvider { + val AuthenticationTokenHeader = ServiceRequestContext.ContextHeaders.AuthenticationTokenHeader + val SetAuthenticationTokenHeader = "set-authorization" } - trait User { - def id: Id[User] - def roles: Set[Role] - def permissions: Set[Permission] = roles.flatMap(_.permissions) - } + trait AuthProvider[U <: User] { - final case class BasicUser(id: Id[User], roles: Set[Role]) extends User + import akka.http.scaladsl.server._ + import Directives._ - final case class AuthToken(value: String) + implicit val execution: ExecutionContext - final case class PasswordHash(value: String) + /** + * Specific implementation on how to extract user from request context, + * can either need to do a network call to auth server or extract everything from self-contained token + * + * @param context set of request values which can be relevant to authenticate user + * @return authenticated user + */ + protected def authenticatedUser(context: ServiceRequestContext): OptionT[Future, U] - object AuthProvider { - val AuthenticationTokenHeader = ServiceRequestContext.ContextHeaders.AuthenticationTokenHeader - val SetAuthenticationTokenHeader = "set-authorization" - } + protected def userHasPermission(user: U, permission: Permission): Future[Boolean] + + def authorize(permissions: Permission*): Directive1[U] = { + ServiceRequestContext.serviceContext flatMap { ctx => - trait AuthProvider[U <: User] { - - import akka.http.scaladsl.server._ - import Directives._ - - /** - * Specific implementation on how to extract user from request context, - * can either need to do a network call to auth server or extract everything from self-contained token - * - * @param context set of request values which can be relevant to authenticate user - * @return authenticated user - */ - protected def authenticatedUser(context: ServiceRequestContext): OptionT[Future, U] - - def authorize(permissions: Permission*): Directive1[U] = { - ServiceRequestContext.serviceContext flatMap { ctx => - onComplete(authenticatedUser(ctx).run).flatMap { - case Success(Some(user)) => - if (permissions.forall(user.permissions.contains)) provide(user) - else { - val challenge = - HttpChallenges.basic(s"User does not have the required permissions: ${permissions.mkString(", ")}") - 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))) + onComplete(authenticatedUser(ctx).run flatMap { userOption => + userOption.traverse[Future, (U, Boolean)] { user => + permissions + .toList + .traverse[Future, Boolean](userHasPermission(user, _)) + .map(results => user -> results.forall(identity)) } + }).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(", ")}") + 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/test/scala/xyz/driver/core/AuthTest.scala b/src/test/scala/xyz/driver/core/AuthTest.scala index 57f79ff..50c8291 100644 --- a/src/test/scala/xyz/driver/core/AuthTest.scala +++ b/src/test/scala/xyz/driver/core/AuthTest.scala @@ -1,13 +1,14 @@ package xyz.driver.core -import akka.http.scaladsl.testkit.ScalatestRouteTest -import akka.http.scaladsl.server._ -import Directives._ 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.testkit.ScalatestRouteTest import org.scalatest.mock.MockitoSugar import org.scalatest.{FlatSpec, Matchers} -import xyz.driver.core.rest.Auth._ +import xyz.driver.core.auth._ +import xyz.driver.core.rest.AuthProvider import xyz.driver.core.rest.ServiceRequestContext import scala.concurrent.Future @@ -18,13 +19,16 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo case object TestRoleAllowedPermission extends Permission case object TestRoleNotAllowedPermission extends Permission - case object TestRole extends Role { - val id = Id("1") - val name = Name("testRole") - val permissions = Set[Permission](TestRoleAllowedPermission) - } + val TestRole = Role(Id("1"), Name("testRole")) val authStatusService: AuthProvider[User] = new AuthProvider[User] { + + override implicit val execution = scala.concurrent.ExecutionContext.global + + override protected def userHasPermission(user: User, permission: Permission): Future[Boolean] = { + Future.successful(permission === TestRoleAllowedPermission) + } + override def authenticatedUser(context: ServiceRequestContext): OptionT[Future, User] = OptionT.optionT[Future] { if (context.contextHeaders.keySet.contains(AuthProvider.AuthenticationTokenHeader)) { Future.successful(Some(BasicUser(Id[User]("1"), Set(TestRole)))) -- cgit v1.2.3