Merge pull request #20010 from ktoso/wip-authorizeAsync-ktoso

+htp #20002 add authorizeAsync
This commit is contained in:
Konrad Malawski 2016-03-17 10:57:18 +01:00
commit f679c9f0b1
8 changed files with 140 additions and 4 deletions

View file

@ -219,7 +219,7 @@ class SecurityDirectivesExamplesSpec extends RoutingSpec {
}
}
"0authorize" in {
"0authorize-0" in {
case class User(name: String)
// authenticate the user:
@ -260,6 +260,48 @@ class SecurityDirectivesExamplesSpec extends RoutingSpec {
}
}
"0authorizeAsync" in {
case class User(name: String)
// authenticate the user:
def myUserPassAuthenticator(credentials: Credentials): Option[User] =
credentials match {
case Credentials.Provided(id) => Some(User(id))
case _ => None
}
// check if user is authorized to perform admin actions,
// this could potentially be a long operation so it would return a Future
val admins = Set("Peter")
def hasAdminPermissions(user: User): Future[Boolean] =
Future.successful(admins.contains(user.name))
val route =
Route.seal {
authenticateBasic(realm = "secure site", myUserPassAuthenticator) { user =>
path("peters-lair") {
authorizeAsync(_ => hasAdminPermissions(user)) {
complete(s"'${user.name}' visited Peter's lair")
}
}
}
}
// tests:
val johnsCred = BasicHttpCredentials("John", "p4ssw0rd")
Get("/peters-lair") ~> addCredentials(johnsCred) ~> // adds Authorization header
route ~> check {
status shouldEqual StatusCodes.Forbidden
responseAs[String] shouldEqual "The supplied authentication is not authorized to access this resource"
}
val petersCred = BasicHttpCredentials("Peter", "pan")
Get("/peters-lair") ~> addCredentials(petersCred) ~> // adds Authorization header
route ~> check {
responseAs[String] shouldEqual "'Peter' visited Peter's lair"
}
}
"0extractCredentials" in {
val route =
extractCredentials { creds =>

View file

@ -24,6 +24,7 @@ Directive Description
a given ``AsyncAuthenticatorPF[T]``
:ref:`-authenticateOrRejectWithChallenge-` Lifts an authenticator function into a directive
:ref:`-authorize-` Applies the given authorization check to the request
:ref:`-authorizeAsync-` Applies the given asynchronous authorization check to the request
:ref:`-cancelRejection-` Adds a ``TransformationRejection`` cancelling all rejections equal to the
given one to the rejections potentially coming back from the inner route.
:ref:`-cancelRejections-` Adds a ``TransformationRejection`` cancelling all matching rejections

View file

@ -24,6 +24,7 @@ If the check returns ``true`` the request is passed on to the inner route unchan
In a common use-case you would check if a user (e.g. supplied by any of the ``authenticate*`` family of directives,
e.g. :ref:`-authenticateBasic-`) is allowed to access the inner routes, e.g. by checking if the user has the needed permissions.
See also :ref:`-authorize-` for the asynchronous version of this directive.
.. note::
See also :ref:`authentication-vs-authorization-scala` to understand the differences between those.
@ -32,4 +33,4 @@ Example
-------
.. includecode2:: ../../../../code/docs/http/scaladsl/server/directives/SecurityDirectivesExamplesSpec.scala
:snippet: 0authorize
:snippet: 0authorize-0

View file

@ -0,0 +1,36 @@
.. _-authorizeAsync-:
authorizeAsync
==============
Signature
---------
.. includecode2:: /../../akka-http/src/main/scala/akka/http/scaladsl/server/directives/SecurityDirectives.scala
:snippet: authorizeAsync
Description
-----------
Applies the given authorization check to the request.
The user-defined authorization check can either be supplied as a ``=> Future[Boolean]`` value which is calculated
just from information out of the lexical scope, or as a function ``RequestContext => Future[Boolean]`` which can also
take information from the request itself into account.
If the check returns ``true`` or the ``Future`` is failed the request is passed on to the inner route unchanged,
otherwise an ``AuthorizationFailedRejection`` is created, triggering a ``403 Forbidden`` response by default
(the same as in the case of an ``AuthenticationFailedRejection``).
In a common use-case you would check if a user (e.g. supplied by any of the ``authenticate*`` family of directives,
e.g. :ref:`-authenticateBasic-`) is allowed to access the inner routes, e.g. by checking if the user has the needed permissions.
See also :ref:`-authorize-` for the synchronous version of this directive.
.. note::
See also :ref:`authentication-vs-authorization-scala` to understand the differences between those.
Example
-------
.. includecode2:: ../../../../code/docs/http/scaladsl/server/directives/SecurityDirectivesExamplesSpec.scala
:snippet: 0authorizeAsync

View file

@ -17,6 +17,7 @@ SecurityDirectives
authenticateOAuth2PFAsync
authenticateOrRejectWithChallenge
authorize
authorizeAsync
extractCredentials

View file

@ -136,4 +136,34 @@ class SecurityDirectivesSpec extends RoutingSpec {
}
}
}
"authorization directives" should {
"authorize" in {
Get() ~> {
authorize(_ true) { complete("OK") }
} ~> check { responseAs[String] shouldEqual "OK" }
}
"not authorize" in {
Get() ~> {
authorize(_ false) { complete("OK") }
} ~> check { rejection shouldEqual AuthorizationFailedRejection }
}
"authorizeAsync" in {
Get() ~> {
authorizeAsync(_ Future.successful(true)) { complete("OK") }
} ~> check { responseAs[String] shouldEqual "OK" }
}
"not authorizeAsync" in {
Get() ~> {
authorizeAsync(_ Future.successful(false)) { complete("OK") }
} ~> check { rejection shouldEqual AuthorizationFailedRejection }
}
"not authorizeAsync when future fails" in {
Get() ~> {
authorizeAsync(_ Future.failed(new Exception("Boom!"))) { complete("OK") }
} ~> check { rejection shouldEqual AuthorizationFailedRejection }
}
}
}

View file

@ -7,4 +7,5 @@ OSGi.http
Dependencies.http
enablePlugins(spray.boilerplate.BoilerplatePlugin)
disablePlugins(MimaPlugin)
scalacOptions in Compile += "-language:_"

View file

@ -13,6 +13,8 @@ import akka.http.scaladsl.util.FastFuture._
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.server.AuthenticationFailedRejection.{ CredentialsRejected, CredentialsMissing }
import scala.util.{Try, Success}
/**
* Provides directives for securing an inner route using the standard Http authentication headers [[`WWW-Authenticate`]]
* and [[Authorization]]. Most prominently, HTTP Basic authentication as defined in RFC 2617.
@ -176,8 +178,30 @@ trait SecurityDirectives {
* If the check fails the route is rejected with an [[AuthorizationFailedRejection]].
*/
def authorize(check: RequestContext Boolean): Directive0 =
extract(check).flatMap[Unit](if (_) pass else reject(AuthorizationFailedRejection)) &
cancelRejection(AuthorizationFailedRejection)
authorizeAsync(ctx => Future.successful(check(ctx)))
/**
* Asynchronous version of [[authorize]].
* If the [[Future]] fails or is completed with `false`
* authorization fails and the route is rejected with an [[AuthorizationFailedRejection]].
*/
def authorizeAsync(check: Future[Boolean]): Directive0 =
authorizeAsync(ctx => check)
/**
* Asynchronous version of [[authorize]].
* If the [[Future]] fails or is completed with `false`
* authorization fails and the route is rejected with an [[AuthorizationFailedRejection]].
*/
def authorizeAsync(check: RequestContext Future[Boolean]): Directive0 =
extractExecutionContext.flatMap { implicit ec
extract(check).flatMap[Unit] { fa =>
onComplete(fa).flatMap {
case Success(true) => pass
case _ => reject(AuthorizationFailedRejection)
}
}
}
/**
* Creates a `Basic` [[HttpChallenge]] for the given realm.