diff --git a/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/SecurityDirectivesExamplesSpec.scala b/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/SecurityDirectivesExamplesSpec.scala index 95e802b836..00981f4f06 100644 --- a/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/SecurityDirectivesExamplesSpec.scala +++ b/akka-docs/rst/scala/code/docs/http/scaladsl/server/directives/SecurityDirectivesExamplesSpec.scala @@ -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 => diff --git a/akka-docs/rst/scala/http/routing-dsl/directives/alphabetically.rst b/akka-docs/rst/scala/http/routing-dsl/directives/alphabetically.rst index 87a02f5e8c..9813f83b53 100644 --- a/akka-docs/rst/scala/http/routing-dsl/directives/alphabetically.rst +++ b/akka-docs/rst/scala/http/routing-dsl/directives/alphabetically.rst @@ -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 diff --git a/akka-docs/rst/scala/http/routing-dsl/directives/security-directives/authorize.rst b/akka-docs/rst/scala/http/routing-dsl/directives/security-directives/authorize.rst index b45faf0ab6..74b8a7237c 100644 --- a/akka-docs/rst/scala/http/routing-dsl/directives/security-directives/authorize.rst +++ b/akka-docs/rst/scala/http/routing-dsl/directives/security-directives/authorize.rst @@ -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 diff --git a/akka-docs/rst/scala/http/routing-dsl/directives/security-directives/authorizeAsync.rst b/akka-docs/rst/scala/http/routing-dsl/directives/security-directives/authorizeAsync.rst new file mode 100644 index 0000000000..57597b1e2d --- /dev/null +++ b/akka-docs/rst/scala/http/routing-dsl/directives/security-directives/authorizeAsync.rst @@ -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 diff --git a/akka-docs/rst/scala/http/routing-dsl/directives/security-directives/index.rst b/akka-docs/rst/scala/http/routing-dsl/directives/security-directives/index.rst index 375c174a92..809d3d7e3c 100644 --- a/akka-docs/rst/scala/http/routing-dsl/directives/security-directives/index.rst +++ b/akka-docs/rst/scala/http/routing-dsl/directives/security-directives/index.rst @@ -17,6 +17,7 @@ SecurityDirectives authenticateOAuth2PFAsync authenticateOrRejectWithChallenge authorize + authorizeAsync extractCredentials diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/SecurityDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/SecurityDirectivesSpec.scala index b5f78310e3..bec592cf40 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/SecurityDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/SecurityDirectivesSpec.scala @@ -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 } + } + } + } diff --git a/akka-http/build.sbt b/akka-http/build.sbt index 91659fdd54..3fcf89b259 100644 --- a/akka-http/build.sbt +++ b/akka-http/build.sbt @@ -7,4 +7,5 @@ OSGi.http Dependencies.http enablePlugins(spray.boilerplate.BoilerplatePlugin) +disablePlugins(MimaPlugin) scalacOptions in Compile += "-language:_" diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/SecurityDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/SecurityDirectives.scala index c316d6f719..6cc08cb2f0 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/SecurityDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/SecurityDirectives.scala @@ -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.