aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/scala/xyz/driver/core/app.scala16
-rw-r--r--src/main/scala/xyz/driver/core/auth.scala133
-rw-r--r--src/main/scala/xyz/driver/core/rest.scala131
-rw-r--r--src/test/scala/xyz/driver/core/AuthTest.scala36
4 files changed, 132 insertions, 184 deletions
diff --git a/src/main/scala/xyz/driver/core/app.scala b/src/main/scala/xyz/driver/core/app.scala
index 9bc34f6..54d08d4 100644
--- a/src/main/scala/xyz/driver/core/app.scala
+++ b/src/main/scala/xyz/driver/core/app.scala
@@ -16,7 +16,8 @@ import org.slf4j.LoggerFactory
import spray.json.DefaultJsonProtocol
import xyz.driver.core
import xyz.driver.core.logging.{Logger, TypesafeScalaLogger}
-import xyz.driver.core.rest.{ContextHeaders, Swagger}
+import xyz.driver.core.rest.ServiceRequestContext.ContextHeaders
+import xyz.driver.core.rest.Swagger
import xyz.driver.core.stats.SystemStats
import xyz.driver.core.time.Time
import xyz.driver.core.time.provider.{SystemTimeProvider, TimeProvider}
@@ -68,7 +69,7 @@ object app {
val _ = Future {
http.bindAndHandle(route2HandlerFlow(handleExceptions(ExceptionHandler(exceptionHandler)) { ctx =>
- val trackingId = rest.extractTrackingId(ctx)
+ val trackingId = rest.ServiceRequestContext.extractTrackingId(ctx)
log.audit(s"Received request ${ctx.request} with tracking id $trackingId")
val contextWithTrackingId =
@@ -81,24 +82,29 @@ object app {
}
}
+ /**
+ * Override me for custom exception handling
+ *
+ * @return Exception handling route for exception type
+ */
protected def exceptionHandler = PartialFunction[Throwable, Route] {
case is: IllegalStateException =>
ctx =>
- val trackingId = rest.extractTrackingId(ctx)
+ val trackingId = rest.ServiceRequestContext.extractTrackingId(ctx)
log.debug(s"Request is not allowed to ${ctx.request.uri} ($trackingId)", is)
complete(HttpResponse(BadRequest, entity = is.getMessage))(ctx)
case cm: ConcurrentModificationException =>
ctx =>
- val trackingId = rest.extractTrackingId(ctx)
+ val trackingId = rest.ServiceRequestContext.extractTrackingId(ctx)
log.audit(s"Concurrent modification of the resource ${ctx.request.uri} ($trackingId)", cm)
complete(
HttpResponse(Conflict, entity = "Resource was changed concurrently, try requesting a newer version"))(ctx)
case t: Throwable =>
ctx =>
- val trackingId = rest.extractTrackingId(ctx)
+ val trackingId = rest.ServiceRequestContext.extractTrackingId(ctx)
log.error(s"Request to ${ctx.request.uri} could not be handled normally ($trackingId)", t)
complete(HttpResponse(InternalServerError, entity = t.getMessage))(ctx)
}
diff --git a/src/main/scala/xyz/driver/core/auth.scala b/src/main/scala/xyz/driver/core/auth.scala
deleted file mode 100644
index cede122..0000000
--- a/src/main/scala/xyz/driver/core/auth.scala
+++ /dev/null
@@ -1,133 +0,0 @@
-package xyz.driver.core
-
-import akka.http.scaladsl.model.headers.HttpChallenges
-import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected
-import xyz.driver.core.rest.ServiceRequestContext
-
-import scala.concurrent.Future
-import scala.util.{Failure, Success}
-import scalaz.OptionT
-
-object auth {
-
- sealed trait Permission
- case object CanSeeUser extends Permission
- case object CanSeeAssay extends Permission
- case object CanSeeReport extends Permission
- case object CanCreateReport extends Permission
- case object CanEditReport extends Permission
- case object CanReviewReport extends Permission
- case object CanEditReviewingReport extends Permission
- case object CanSignOutReport extends Permission
- case object CanAmendReport extends Permission
- case object CanShareReportWithPatient extends Permission
- case object CanAssignRoles extends Permission
-
- trait Role {
- val id: Id[Role]
- val name: Name[Role]
- val permissions: Set[Permission]
-
- def hasPermission(permission: Permission): Boolean = permissions.contains(permission)
- }
-
- object Role {
- def fromString(roleString: String): Option[Role] = roleString match {
- case "Observer" => Some(ObserverRole)
- case "Patient" => Some(PatientRole)
- case "Curator" => Some(CuratorRole)
- case "Pathologist" => Some(PathologistRole)
- case "Administrator" => Some(AdministratorRole)
- case "Physician" => Some(PhysicianRole)
- case "Relative" => Some(RelativeRole)
- case _ => None
- }
- }
-
- case object ObserverRole extends Role {
- val id = Id("1")
- val name = Name("observer")
- val permissions = Set[Permission](CanSeeUser, CanSeeAssay, CanSeeReport)
- }
-
- case object PatientRole extends Role {
- val id = Id("2")
- val name = Name("patient")
- val permissions = Set.empty[Permission]
- }
-
- case object CuratorRole extends Role {
- val id = Id("3")
- val name = Name("curator")
- val permissions = ObserverRole.permissions ++ Set[Permission](CanEditReport, CanReviewReport)
- }
-
- case object PathologistRole extends Role {
- val id = Id("4")
- val name = Name("pathologist")
- val permissions = ObserverRole.permissions ++
- Set[Permission](CanEditReport, CanSignOutReport, CanAmendReport, CanEditReviewingReport)
- }
-
- case object AdministratorRole extends Role {
- val id = Id("5")
- val name = Name("administrator")
- val permissions = CuratorRole.permissions ++
- Set[Permission](CanCreateReport, CanShareReportWithPatient, CanAssignRoles)
- }
-
- case object PhysicianRole extends Role {
- val id = Id("6")
- val name = Name("physician")
- val permissions = Set[Permission]()
- }
-
- case object RelativeRole extends Role {
- val id = Id("7")
- val name = Name("relative")
- val permissions = Set[Permission]()
- }
-
- trait User {
- def id: Id[User]
- def roles: Set[Role]
- def permissions: Set[Permission] = roles.flatMap(_.permissions)
- }
-
- final case class AuthToken(value: String)
-
- final case class PasswordHash(value: String)
-
- object AuthService {
- val AuthenticationTokenHeader = rest.ContextHeaders.AuthenticationTokenHeader
- val SetAuthenticationTokenHeader = "set-authorization"
- }
-
- trait AuthService[U <: User] {
-
- import akka.http.scaladsl.server._
- import Directives._
-
- protected def authStatus(context: ServiceRequestContext): OptionT[Future, U]
-
- def authorize(permissions: Permission*): Directive1[U] = {
- rest.serviceContext flatMap { ctx =>
- onComplete(authStatus(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)))
- }
- }
- }
- }
-}
diff --git a/src/main/scala/xyz/driver/core/rest.scala b/src/main/scala/xyz/driver/core/rest.scala
index 554fe75..437df3c 100644
--- a/src/main/scala/xyz/driver/core/rest.scala
+++ b/src/main/scala/xyz/driver/core/rest.scala
@@ -3,14 +3,14 @@ package xyz.driver.core
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
-import akka.http.scaladsl.model.headers.RawHeader
+import akka.http.scaladsl.model.headers.{HttpChallenges, RawHeader}
+import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected
import akka.http.scaladsl.unmarshalling.Unmarshal
import akka.stream.ActorMaterializer
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
@@ -18,50 +18,119 @@ import xyz.driver.core.time.provider.TimeProvider
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}
+import scalaz.OptionT
import scalaz.Scalaz.{Id => _, _}
object rest {
- object ContextHeaders {
- val AuthenticationTokenHeader = "WWW-Authenticate"
- val TrackingIdHeader = "X-Trace"
-
- object LinkerD {
- // https://linkerd.io/doc/0.7.4/linkerd/protocol-http/
- def isLinkerD(headerName: String) = headerName.startsWith("l5d-")
- }
- }
-
final case class ServiceRequestContext(
trackingId: String = generators.nextUuid().toString,
contextHeaders: Map[String, String] = Map.empty[String, String]) {
- def authToken: Option[AuthToken] = contextHeaders.get(AuthService.AuthenticationTokenHeader).map(AuthToken.apply)
+ def authToken: Option[Auth.AuthToken] =
+ contextHeaders.get(Auth.AuthProvider.AuthenticationTokenHeader).map(Auth.AuthToken.apply)
}
- import akka.http.scaladsl.server._
- import Directives._
+ object ServiceRequestContext {
- def serviceContext: Directive1[ServiceRequestContext] = extract(ctx => extractServiceContext(ctx))
+ object ContextHeaders {
+ val AuthenticationTokenHeader = "WWW-Authenticate"
+ val TrackingIdHeader = "X-Trace"
- def extractServiceContext(ctx: RequestContext): ServiceRequestContext =
- ServiceRequestContext(extractTrackingId(ctx), extractContextHeaders(ctx))
+ object LinkerD {
+ // https://linkerd.io/doc/0.7.4/linkerd/protocol-http/
+ def isLinkerD(headerName: String) = headerName.startsWith("l5d-")
+ }
+ }
- def extractTrackingId(ctx: RequestContext): String = {
- ctx.request.headers
- .find(_.name == ContextHeaders.TrackingIdHeader)
- .fold(java.util.UUID.randomUUID.toString)(_.value())
- }
+ import akka.http.scaladsl.server._
+ import Directives._
- def extractContextHeaders(ctx: RequestContext): Map[String, String] = {
- ctx.request.headers.filter { h =>
- h.name === ContextHeaders.AuthenticationTokenHeader || h.name === ContextHeaders.TrackingIdHeader
- // || ContextHeaders.LinkerD.isLinkerD(h.lowercaseName)
- } map { header =>
- header.name -> header.value
- } toMap
+ def serviceContext: Directive1[ServiceRequestContext] = extract(ctx => extractServiceContext(ctx))
+
+ def extractServiceContext(ctx: RequestContext): ServiceRequestContext =
+ ServiceRequestContext(extractTrackingId(ctx), extractContextHeaders(ctx))
+
+ def extractTrackingId(ctx: RequestContext): String = {
+ ctx.request.headers
+ .find(_.name == ContextHeaders.TrackingIdHeader)
+ .fold(java.util.UUID.randomUUID.toString)(_.value())
+ }
+
+ def extractContextHeaders(ctx: RequestContext): Map[String, String] = {
+ ctx.request.headers.filter { h =>
+ h.name === ContextHeaders.AuthenticationTokenHeader || h.name === ContextHeaders.TrackingIdHeader
+ // || ContextHeaders.LinkerD.isLinkerD(h.lowercaseName)
+ } map { header =>
+ header.name -> header.value
+ } toMap
+ }
}
+ 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)
+ }
+
+ trait User {
+ def id: Id[User]
+ def roles: Set[Role]
+ def permissions: Set[Permission] = roles.flatMap(_.permissions)
+ }
+
+ final case class BasicUser(id: Id[User], roles: Set[Role]) extends User
+
+ final case class AuthToken(value: String)
+
+ final case class PasswordHash(value: String)
+
+ object AuthProvider {
+ val AuthenticationTokenHeader = ServiceRequestContext.ContextHeaders.AuthenticationTokenHeader
+ val SetAuthenticationTokenHeader = "set-authorization"
+ }
+
+ 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)))
+ }
+ }
+ }
+ }
+ }
trait Service
@@ -86,7 +155,7 @@ object rest {
val requestTime = time.currentTime()
val request = requestStub
- .withHeaders(RawHeader(ContextHeaders.TrackingIdHeader, context.trackingId))
+ .withHeaders(RawHeader(ServiceRequestContext.ContextHeaders.TrackingIdHeader, context.trackingId))
.withHeaders(context.contextHeaders.toSeq.map { h => RawHeader(h._1, h._2): HttpHeader }: _*)
log.audit(s"Sending to ${request.uri} request $request with tracking id ${context.trackingId}")
diff --git a/src/test/scala/xyz/driver/core/AuthTest.scala b/src/test/scala/xyz/driver/core/AuthTest.scala
index f4d4d2a..57f79ff 100644
--- a/src/test/scala/xyz/driver/core/AuthTest.scala
+++ b/src/test/scala/xyz/driver/core/AuthTest.scala
@@ -7,7 +7,7 @@ import akka.http.scaladsl.model.headers.{HttpChallenges, RawHeader}
import akka.http.scaladsl.server.AuthenticationFailedRejection.CredentialsRejected
import org.scalatest.mock.MockitoSugar
import org.scalatest.{FlatSpec, Matchers}
-import xyz.driver.core.auth._
+import xyz.driver.core.rest.Auth._
import xyz.driver.core.rest.ServiceRequestContext
import scala.concurrent.Future
@@ -15,13 +15,19 @@ import scalaz.OptionT
class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRouteTest {
- val authStatusService: AuthService[User] = new AuthService[User] {
- override def authStatus(context: ServiceRequestContext): OptionT[Future, User] = OptionT.optionT[Future] {
- if (context.contextHeaders.keySet.contains(AuthService.AuthenticationTokenHeader)) {
- Future.successful(Some(new User {
- override def id: Id[User] = Id[User]("1")
- override def roles: Set[Role] = Set(PathologistRole)
- }: User))
+ 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 authStatusService: AuthProvider[User] = new AuthProvider[User] {
+ 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))))
} else {
Future.successful(Option.empty[User])
}
@@ -33,7 +39,7 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo
"'authorize' directive" should "throw error is auth token is not in the request" in {
Get("/naive/attempt") ~>
- authorize(CanSignOutReport) { user =>
+ authorize(TestRoleAllowedPermission) { user =>
complete("Never going to be here")
} ~>
check {
@@ -44,12 +50,12 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo
it should "throw error is authorized user is not having the requested permission" in {
- val referenceAuthToken = AuthToken("I am a pathologist's token")
+ val referenceAuthToken = AuthToken("I am a test role's token")
Post("/administration/attempt").addHeader(
- RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value)
+ RawHeader(AuthProvider.AuthenticationTokenHeader, referenceAuthToken.value)
) ~>
- authorize(CanAssignRoles) { user =>
+ authorize(TestRoleNotAllowedPermission) { user =>
complete("Never going to get here")
} ~>
check {
@@ -57,7 +63,7 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo
rejections should contain(
AuthenticationFailedRejection(
CredentialsRejected,
- HttpChallenges.basic("User does not have the required permissions: CanAssignRoles")))
+ HttpChallenges.basic("User does not have the required permissions: TestRoleNotAllowedPermission")))
}
}
@@ -66,9 +72,9 @@ class AuthTest extends FlatSpec with Matchers with MockitoSugar with ScalatestRo
val referenceAuthToken = AuthToken("I am token")
Get("/valid/attempt/?a=2&b=5").addHeader(
- RawHeader(AuthService.AuthenticationTokenHeader, referenceAuthToken.value)
+ RawHeader(AuthProvider.AuthenticationTokenHeader, referenceAuthToken.value)
) ~>
- authorize(CanSignOutReport) { user =>
+ authorize(TestRoleAllowedPermission) { user =>
complete("Alright, user \"" + user.id + "\" is authorized")
} ~>
check {