From 3008753cf543caaecb7d0e325c9f4473ad8a0322 Mon Sep 17 00:00:00 2001 From: vlad Date: Tue, 2 Aug 2016 10:54:00 -0700 Subject: Domain model responsible for auth is in core + Akka-http auth directives + More specific REST service API --- src/main/scala/com/drivergrp/core/auth.scala | 100 +++++++++++++++++++++++++ src/main/scala/com/drivergrp/core/crypto.scala | 8 +- src/main/scala/com/drivergrp/core/file.scala | 12 +++ src/main/scala/com/drivergrp/core/rest.scala | 79 ++++++++----------- 4 files changed, 145 insertions(+), 54 deletions(-) create mode 100644 src/main/scala/com/drivergrp/core/auth.scala create mode 100644 src/main/scala/com/drivergrp/core/file.scala (limited to 'src/main') diff --git a/src/main/scala/com/drivergrp/core/auth.scala b/src/main/scala/com/drivergrp/core/auth.scala new file mode 100644 index 0000000..84d943d --- /dev/null +++ b/src/main/scala/com/drivergrp/core/auth.scala @@ -0,0 +1,100 @@ +package com.drivergrp.core + +object auth { + + final case class FullName[+T](firstName: Name[T], middleName: Name[T], lastName: Name[T]) + + final case class Email(username: String, domain: String) { + override def toString = username + "@" + domain + } + + trait Role { + val id: Id[Role] + val name: Name[Role] + + def canEditReport: Boolean = false + def canSignOffReport: Boolean = false + def canAssignRoles: Boolean = false + } + + case object ObserverRole extends Role { + val id = Id(1L) + val name = Name("observer") + } + + case object PatientRole extends Role { + val id = Id(2L) + val name = Name("patient") + } + + case object CuratorRole extends Role { + val id = Id(3L) + val name = Name("curator") + + override def canEditReport: Boolean = true + } + + case object PathologistRole extends Role { + val id = Id(4L) + val name = Name("pathologist") + + override def canEditReport: Boolean = true + override def canSignOffReport: Boolean = true + } + + case object AdministratorRole extends Role { + val id = Id(5L) + val name = Name("administrator") + + override def canEditReport: Boolean = true + override def canSignOffReport: Boolean = true + override def canAssignRoles: Boolean = true + } + + final case class Avatar(id: Id[Avatar], name: Name[Avatar]) + + final case class User(id: Id[User], name: FullName[User], email: Email, avatar: Option[Avatar], roles: Set[Role]) + + val TestUser = User(Id[User](1L), + FullName[User](Name("James"), Name("Dewey"), Name("Watson")), + Email("j.watson", "uchicago.edu"), + Some(Avatar(Id[Avatar](1L), Name[Avatar]("Coolface"))), + Set(PathologistRole)) + + final case class Macaroon(value: String) + + final case class Base64[T](value: String) + + final case class AuthToken(value: Base64[Macaroon]) + + object directives { + import akka.http.scaladsl.server._ + import Directives._ + + val AuthenticationTokenHeader = "WWW-Authenticate" + + def authorize(role: Role): Directive1[Id[User]] = { + headerValueByName(AuthenticationTokenHeader).flatMap { tokenValue => + val token = AuthToken(Base64[Macaroon](tokenValue)) + + extractUser(token) match { + case Some(user) => + if (user.roles.contains(role)) provide(user.id) + else reject(ValidationRejection(s"User does not have the required ${role.name} role")) + case None => + reject(ValidationRejection(s"Wasn't able to extract user for the token provided")) + } + } + } + + def extractToken: Directive1[AuthToken] = { + headerValueByName(AuthenticationTokenHeader).flatMap { token => + provide(AuthToken(Base64[Macaroon](token))) + } + } + + def extractUser(authToken: AuthToken): Option[User] = { + Some(TestUser) + } + } +} diff --git a/src/main/scala/com/drivergrp/core/crypto.scala b/src/main/scala/com/drivergrp/core/crypto.scala index 2910260..f693fa3 100644 --- a/src/main/scala/com/drivergrp/core/crypto.scala +++ b/src/main/scala/com/drivergrp/core/crypto.scala @@ -1,12 +1,8 @@ package com.drivergrp.core -object crypto { - - final case class Macaroon(value: String) +import com.drivergrp.core.auth.AuthToken - final case class Base64[T](value: String) - - final case class AuthToken(value: Base64[Macaroon]) +object crypto { final case class EncryptionKey(value: String) diff --git a/src/main/scala/com/drivergrp/core/file.scala b/src/main/scala/com/drivergrp/core/file.scala new file mode 100644 index 0000000..e9340ff --- /dev/null +++ b/src/main/scala/com/drivergrp/core/file.scala @@ -0,0 +1,12 @@ +package com.drivergrp.core + +import com.drivergrp.core.time.Time + +object file { + + final case class Document( + id: Id[Document], + name: Name[Document], + additionDate: Time + ) +} diff --git a/src/main/scala/com/drivergrp/core/rest.scala b/src/main/scala/com/drivergrp/core/rest.scala index 4edb466..ebb2640 100644 --- a/src/main/scala/com/drivergrp/core/rest.scala +++ b/src/main/scala/com/drivergrp/core/rest.scala @@ -2,14 +2,14 @@ package com.drivergrp.core import akka.actor.ActorSystem import akka.http.scaladsl.Http -import akka.http.scaladsl.marshalling.{Marshal, Marshaller} import akka.http.scaladsl.model._ import akka.http.scaladsl.model.headers.RawHeader -import akka.http.scaladsl.unmarshalling.{Unmarshal, Unmarshaller} +import akka.http.scaladsl.unmarshalling.Unmarshal import akka.stream.ActorMaterializer import akka.stream.scaladsl.Flow -import akka.util.{ByteString, Timeout} -import com.drivergrp.core.crypto.{AuthToken, Crypto} +import akka.util.ByteString +import com.drivergrp.core.auth.AuthToken +import com.drivergrp.core.crypto.Crypto import com.drivergrp.core.logging.Logger import com.drivergrp.core.stats.Stats import com.drivergrp.core.time.TimeRange @@ -18,12 +18,10 @@ import com.github.swagger.akka.model._ import com.github.swagger.akka.{HasActorSystem, SwaggerHttpService} import com.typesafe.config.Config -import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} -import scala.language.postfixOps import scala.util.{Failure, Success} -import scalaz.{Failure => _, Success => _} import scalaz.Scalaz._ +import scalaz.{Failure => _, Success => _} object rest { @@ -32,29 +30,25 @@ object rest { this.majorVersion === otherVersion.majorVersion } - trait Service { + type Service = AnyRef - def sendRequest[I,O](authToken: AuthToken)(requestInput: I) - (implicit marshaller: Marshaller[I, RequestEntity], - unmarshaller: Unmarshaller[ResponseEntity, O]): Future[O] + trait ServiceTransport { + + def sendRequest(authToken: AuthToken)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] } - trait ServiceDiscovery { + trait ServiceDiscovery[T <: Service] { - def discover(serviceName: Name[Service], version: ServiceVersion): Service + def discover(serviceName: Name[Service], version: ServiceVersion): T } - class HttpRestService(method: HttpMethod, uri: Uri, version: ServiceVersion, - actorSystem: ActorSystem, executionContext: ExecutionContext, - crypto: Crypto, log: Logger, stats: Stats, time: TimeProvider) extends Service { + class HttpRestServiceTransport(actorSystem: ActorSystem, executionContext: ExecutionContext, + crypto: Crypto, log: Logger, stats: Stats, time: TimeProvider) extends ServiceTransport { protected implicit val materializer = ActorMaterializer()(actorSystem) protected implicit val execution = executionContext - protected implicit val timeout = Timeout(5 seconds) - def sendRequest[I,O](authToken: AuthToken)(requestInput: I) - (implicit marshaller: Marshaller[I, RequestEntity], - unmarshaller: Unmarshaller[ResponseEntity, O]): Future[O] = { + def sendRequest(authToken: AuthToken)(requestStub: HttpRequest): Future[Unmarshal[ResponseEntity]] = { val requestTime = time.currentTime() val encryptionFlow = Flow[ByteString] map { bytes => @@ -64,43 +58,32 @@ object rest { ByteString(crypto.decrypt(crypto.keyForToken(authToken))(bytes.toArray)) } - val response: Future[O] = for { - requestData: RequestEntity <- Marshal(requestInput).to[RequestEntity](marshaller, executionContext) - encryptedMessage = requestData.transformDataBytes(encryptionFlow) - request: HttpRequest = buildRequest(authToken, requestData) - _ = log.audit(s"Sending to ${request.uri} request $request") - response <- Http()(actorSystem).singleRequest(request)(materializer) - decryptedResponse = requestData.transformDataBytes(decryptionFlow) - responseEntity <- Unmarshal(decryptedResponse).to[O](unmarshaller, executionContext, materializer) - } yield { - responseEntity + val request = requestStub + .withEntity(requestStub.entity.transformDataBytes(encryptionFlow)) + .withHeaders( + RawHeader(auth.directives.AuthenticationTokenHeader, s"Macaroon ${authToken.value.value}")) + + log.audit(s"Sending to ${request.uri} request $request") + + val responseEntity = Http()(actorSystem).singleRequest(request)(materializer) map { response => + if(response.status.isFailure()) throw new Exception("Http status is failure " + response.status) + else Unmarshal(response.entity.transformDataBytes(decryptionFlow)) } - response.onComplete { + responseEntity.onComplete { case Success(r) => val responseTime = time.currentTime() - log.audit(s"Response from $uri to request $requestInput is successful") - stats.recordStats(Seq("request", uri.toString, "success"), TimeRange(requestTime, responseTime), 1) + log.audit(s"Response from ${request.uri} to request $requestStub is successful") + stats.recordStats(Seq("request", request.uri.toString, "success"), TimeRange(requestTime, responseTime), 1) case Failure(t: Throwable) => val responseTime = time.currentTime() - log.audit(s"Failed to receive response from $uri of version $version to request $requestInput") - log.error(s"Failed to receive response from $uri of version $version to request $requestInput", t) - stats.recordStats(Seq("request", uri.toString, "fail"), TimeRange(requestTime, responseTime), 1) + log.audit(s"Failed to receive response from ${request.uri} to request $requestStub") + log.error(s"Failed to receive response from ${request.uri} to request $requestStub", t) + stats.recordStats(Seq("request", request.uri.toString, "fail"), TimeRange(requestTime, responseTime), 1) } (executionContext) - response - } - - private def buildRequest(authToken: AuthToken, requestData: RequestEntity): HttpRequest = { - - HttpRequest( - method, uri, - headers = Vector( - RawHeader("WWW-Authenticate", s"Macaroon ${authToken.value.value}"), - RawHeader("Api-Version", version.majorVersion + "." + version.minorVersion) - ), - entity = requestData) + responseEntity } } -- cgit v1.2.3