+doc example snippet for akka http java dsl: SecurityDirectives (#20717)

This commit is contained in:
Hawstein 2016-06-19 08:06:19 +08:00 committed by Konrad Malawski
parent f246c56087
commit cc22ed4560
13 changed files with 442 additions and 11 deletions

View file

@ -0,0 +1,364 @@
/*
* Copyright (C) 2016-2016 Lightbend Inc. <http://www.lightbend.com>
*/
package docs.http.javadsl.server.directives;
import akka.http.javadsl.model.HttpRequest;
import akka.http.javadsl.model.StatusCodes;
import akka.http.javadsl.model.headers.BasicHttpCredentials;
import akka.http.javadsl.model.headers.HttpChallenge;
import akka.http.javadsl.model.headers.HttpCredentials;
import akka.http.javadsl.server.Route;
import akka.http.javadsl.testkit.JUnitRouteTest;
import akka.japi.JavaPartialFunction;
import org.junit.Test;
import scala.PartialFunction;
import scala.util.Either;
import scala.util.Left;
import scala.util.Right;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;
import java.util.Optional;
public class SecurityDirectivesExamplesTest extends JUnitRouteTest {
@Test
public void testAuthenticateBasic() {
//#authenticateBasic
final Function<Optional<ProvidedCredentials>, Optional<String>> myUserPassAuthenticator =
credentials ->
credentials.filter(c -> c.verify("p4ssw0rd")).map(ProvidedCredentials::identifier);
final Route route = path("secured", () ->
authenticateBasic("secure site", myUserPassAuthenticator, userName ->
complete("The user is '" + userName + "'")
)
).seal(system(), materializer());
// tests:
testRoute(route).run(HttpRequest.GET("/secured"))
.assertStatusCode(StatusCodes.UNAUTHORIZED)
.assertEntity("The resource requires authentication, which was not supplied with the request")
.assertHeaderExists("WWW-Authenticate", "Basic realm=\"secure site\"");
final HttpCredentials validCredentials =
BasicHttpCredentials.createBasicHttpCredentials("John", "p4ssw0rd");
testRoute(route).run(HttpRequest.GET("/secured").addCredentials(validCredentials))
.assertEntity("The user is 'John'");
final HttpCredentials invalidCredentials =
BasicHttpCredentials.createBasicHttpCredentials("Peter", "pan");
testRoute(route).run(HttpRequest.GET("/secured").addCredentials(invalidCredentials))
.assertStatusCode(StatusCodes.UNAUTHORIZED)
.assertEntity("The supplied authentication is invalid")
.assertHeaderExists("WWW-Authenticate", "Basic realm=\"secure site\"");
//#authenticateBasic
}
@Test
public void testAuthenticateBasicPF() {
//#authenticateBasicPF
final PartialFunction<Optional<ProvidedCredentials>, String> myUserPassAuthenticator =
new JavaPartialFunction<Optional<ProvidedCredentials>, String>() {
@Override
public String apply(Optional<ProvidedCredentials> opt, boolean isCheck) throws Exception {
if (opt.filter(c -> (c != null) && c.verify("p4ssw0rd")).isPresent()) {
if (isCheck) return null;
else return opt.get().identifier();
} else if (opt.filter(c -> (c != null) && c.verify("p4ssw0rd-special")).isPresent()) {
if (isCheck) return null;
else return opt.get().identifier() + "-admin";
} else {
throw noMatch();
}
}
};
final Route route = path("secured", () ->
authenticateBasicPF("secure site", myUserPassAuthenticator, userName ->
complete("The user is '" + userName + "'")
)
).seal(system(), materializer());
// tests:
testRoute(route).run(HttpRequest.GET("/secured"))
.assertStatusCode(StatusCodes.UNAUTHORIZED)
.assertEntity("The resource requires authentication, which was not supplied with the request")
.assertHeaderExists("WWW-Authenticate", "Basic realm=\"secure site\"");
final HttpCredentials validCredentials =
BasicHttpCredentials.createBasicHttpCredentials("John", "p4ssw0rd");
testRoute(route).run(HttpRequest.GET("/secured").addCredentials(validCredentials))
.assertEntity("The user is 'John'");
final HttpCredentials validAdminCredentials =
BasicHttpCredentials.createBasicHttpCredentials("John", "p4ssw0rd-special");
testRoute(route).run(HttpRequest.GET("/secured").addCredentials(validAdminCredentials))
.assertEntity("The user is 'John-admin'");
final HttpCredentials invalidCredentials =
BasicHttpCredentials.createBasicHttpCredentials("Peter", "pan");
testRoute(route).run(HttpRequest.GET("/secured").addCredentials(invalidCredentials))
.assertStatusCode(StatusCodes.UNAUTHORIZED)
.assertEntity("The supplied authentication is invalid")
.assertHeaderExists("WWW-Authenticate", "Basic realm=\"secure site\"");
//#authenticateBasicPF
}
@Test
public void testAuthenticateBasicPFAsync() {
//#authenticateBasicPFAsync
class User {
private final String id;
public User(String id) {
this.id = id;
}
public String getId() {
return id;
}
}
final PartialFunction<Optional<ProvidedCredentials>, CompletionStage<User>> myUserPassAuthenticator =
new JavaPartialFunction<Optional<ProvidedCredentials>,CompletionStage<User>>() {
@Override
public CompletionStage<User> apply(Optional<ProvidedCredentials> opt, boolean isCheck) throws Exception {
if (opt.filter(c -> (c != null) && c.verify("p4ssw0rd")).isPresent()) {
if (isCheck) return CompletableFuture.completedFuture(null);
else return CompletableFuture.completedFuture(new User(opt.get().identifier()));
} else {
throw noMatch();
}
}
};
final Route route = path("secured", () ->
authenticateBasicPFAsync("secure site", myUserPassAuthenticator, user ->
complete("The user is '" + user.getId() + "'"))
).seal(system(), materializer());
// tests:
testRoute(route).run(HttpRequest.GET("/secured"))
.assertStatusCode(StatusCodes.UNAUTHORIZED)
.assertEntity("The resource requires authentication, which was not supplied with the request")
.assertHeaderExists("WWW-Authenticate", "Basic realm=\"secure site\"");
final HttpCredentials validCredentials =
BasicHttpCredentials.createBasicHttpCredentials("John", "p4ssw0rd");
testRoute(route).run(HttpRequest.GET("/secured").addCredentials(validCredentials))
.assertEntity("The user is 'John'");
final HttpCredentials invalidCredentials =
BasicHttpCredentials.createBasicHttpCredentials("Peter", "pan");
testRoute(route).run(HttpRequest.GET("/secured").addCredentials(invalidCredentials))
.assertStatusCode(StatusCodes.UNAUTHORIZED)
.assertEntity("The supplied authentication is invalid")
.assertHeaderExists("WWW-Authenticate", "Basic realm=\"secure site\"");
//#authenticateBasicPFAsync
}
@Test
public void testAuthenticateBasicAsync() {
//#authenticateBasicAsync
final Function<Optional<ProvidedCredentials>, CompletionStage<Optional<String>>> myUserPassAuthenticator = opt -> {
if (opt.filter(c -> (c != null) && c.verify("p4ssw0rd")).isPresent()) {
return CompletableFuture.completedFuture(Optional.of(opt.get().identifier()));
} else {
return CompletableFuture.completedFuture(Optional.empty());
}
};
final Route route = path("secured", () ->
authenticateBasicAsync("secure site", myUserPassAuthenticator, userName ->
complete("The user is '" + userName + "'")
)
).seal(system(), materializer());
// tests:
testRoute(route).run(HttpRequest.GET("/secured"))
.assertStatusCode(StatusCodes.UNAUTHORIZED)
.assertEntity("The resource requires authentication, which was not supplied with the request")
.assertHeaderExists("WWW-Authenticate", "Basic realm=\"secure site\"");
final HttpCredentials validCredentials =
BasicHttpCredentials.createBasicHttpCredentials("John", "p4ssw0rd");
testRoute(route).run(HttpRequest.GET("/secured").addCredentials(validCredentials))
.assertEntity("The user is 'John'");
final HttpCredentials invalidCredentials =
BasicHttpCredentials.createBasicHttpCredentials("Peter", "pan");
testRoute(route).run(HttpRequest.GET("/secured").addCredentials(invalidCredentials))
.assertStatusCode(StatusCodes.UNAUTHORIZED)
.assertEntity("The supplied authentication is invalid")
.assertHeaderExists("WWW-Authenticate", "Basic realm=\"secure site\"");
//#authenticateBasicAsync
}
@Test
public void testAuthenticateOrRejectWithChallenge() {
//#authenticateOrRejectWithChallenge
final HttpChallenge challenge = HttpChallenge.create("MyAuth", "MyRealm");
// your custom authentication logic:
final Function<HttpCredentials, Boolean> auth = credentials -> true;
final Function<Optional<HttpCredentials>, CompletionStage<Either<HttpChallenge, String>>> myUserPassAuthenticator =
opt -> {
if (opt.isPresent() && auth.apply(opt.get())) {
return CompletableFuture.completedFuture(Right.apply("some-user-name-from-creds"));
} else {
return CompletableFuture.completedFuture(Left.apply(challenge));
}
};
final Route route = path("secured", () ->
authenticateOrRejectWithChallenge(myUserPassAuthenticator, userName ->
complete("Authenticated!")
)
).seal(system(), materializer());
// tests:
testRoute(route).run(HttpRequest.GET("/secured"))
.assertStatusCode(StatusCodes.UNAUTHORIZED)
.assertEntity("The resource requires authentication, which was not supplied with the request")
.assertHeaderExists("WWW-Authenticate", "MyAuth realm=\"MyRealm\"");
final HttpCredentials validCredentials =
BasicHttpCredentials.createBasicHttpCredentials("John", "p4ssw0rd");
testRoute(route).run(HttpRequest.GET("/secured").addCredentials(validCredentials))
.assertStatusCode(StatusCodes.OK)
.assertEntity("Authenticated!");
//#authenticateOrRejectWithChallenge
}
@Test
public void testAuthorize() {
//#authorize
class User {
private final String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
// authenticate the user:
final Function<Optional<ProvidedCredentials>, Optional<User>> myUserPassAuthenticator =
opt -> {
if (opt.isPresent()) {
return Optional.of(new User(opt.get().identifier()));
} else {
return Optional.empty();
}
};
// check if user is authorized to perform admin actions:
final Set<String> admins = new HashSet<>();
admins.add("Peter");
final Function<User, Boolean> hasAdminPermissions = user -> admins.contains(user.getName());
final Route route = authenticateBasic("secure site", myUserPassAuthenticator, user ->
path("peters-lair", () ->
authorize(() -> hasAdminPermissions.apply(user), () ->
complete("'" + user.getName() +"' visited Peter's lair")
)
)
).seal(system(), materializer());
// tests:
final HttpCredentials johnsCred =
BasicHttpCredentials.createBasicHttpCredentials("John", "p4ssw0rd");
testRoute(route).run(HttpRequest.GET("/peters-lair").addCredentials(johnsCred))
.assertStatusCode(StatusCodes.FORBIDDEN)
.assertEntity("The supplied authentication is not authorized to access this resource");
final HttpCredentials petersCred =
BasicHttpCredentials.createBasicHttpCredentials("Peter", "pan");
testRoute(route).run(HttpRequest.GET("/peters-lair").addCredentials(petersCred))
.assertEntity("'Peter' visited Peter's lair");
//#authorize
}
@Test
public void testAuthorizeAsync() {
//#authorizeAsync
class User {
private final String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
// authenticate the user:
final Function<Optional<ProvidedCredentials>, Optional<User>> myUserPassAuthenticator =
opt -> {
if (opt.isPresent()) {
return Optional.of(new User(opt.get().identifier()));
} else {
return Optional.empty();
}
};
// check if user is authorized to perform admin actions,
// this could potentially be a long operation so it would return a Future
final Set<String> admins = new HashSet<>();
admins.add("Peter");
final Set<String> synchronizedAdmins = Collections.synchronizedSet(admins);
final Function<User, CompletionStage<Object>> hasAdminPermissions =
user -> CompletableFuture.completedFuture(synchronizedAdmins.contains(user.getName()));
final Route route = authenticateBasic("secure site", myUserPassAuthenticator, user ->
path("peters-lair", () ->
authorizeAsync(() -> hasAdminPermissions.apply(user), () ->
complete("'" + user.getName() +"' visited Peter's lair")
)
)
).seal(system(), materializer());
// tests:
final HttpCredentials johnsCred =
BasicHttpCredentials.createBasicHttpCredentials("John", "p4ssw0rd");
testRoute(route).run(HttpRequest.GET("/peters-lair").addCredentials(johnsCred))
.assertStatusCode(StatusCodes.FORBIDDEN)
.assertEntity("The supplied authentication is not authorized to access this resource");
final HttpCredentials petersCred =
BasicHttpCredentials.createBasicHttpCredentials("Peter", "pan");
testRoute(route).run(HttpRequest.GET("/peters-lair").addCredentials(petersCred))
.assertEntity("'Peter' visited Peter's lair");
//#authorizeAsync
}
@Test
public void testExtractCredentials() {
//#extractCredentials
final Route route = extractCredentials(optCreds -> {
if (optCreds.isPresent()) {
return complete("Credentials: " + optCreds.get());
} else {
return complete("No credentials");
}
});
// tests:
final HttpCredentials johnsCred =
BasicHttpCredentials.createBasicHttpCredentials("John", "p4ssw0rd");
testRoute(route).run(HttpRequest.GET("/").addCredentials(johnsCred))
.assertEntity("Credentials: Basic Sm9objpwNHNzdzByZA==");
testRoute(route).run(HttpRequest.GET("/"))
.assertEntity("No credentials");
//#extractCredentials
}
}

View file

@ -27,4 +27,5 @@ See :ref:`credentials-and-timing-attacks-java` for details about verifying the s
Example
-------
TODO: Example snippets for JavaDSL are subject to community contributions! Help us complete the docs, read more about it here: `write example snippets for Akka HTTP Java DSL #20466 <https://github.com/akka/akka/issues/20466>`_.
.. includecode:: ../../../../code/docs/http/javadsl/server/directives/SecurityDirectivesExamplesTest.java#authenticateBasic

View file

@ -25,4 +25,5 @@ See :ref:`credentials-and-timing-attacks-java` for details about verifying the s
Example
-------
TODO: Example snippets for JavaDSL are subject to community contributions! Help us complete the docs, read more about it here: `write example snippets for Akka HTTP Java DSL #20466 <https://github.com/akka/akka/issues/20466>`_.
.. includecode:: ../../../../code/docs/http/javadsl/server/directives/SecurityDirectivesExamplesTest.java#authenticateBasicAsync

View file

@ -25,4 +25,5 @@ See :ref:`credentials-and-timing-attacks-java` for details about verifying the s
Example
-------
TODO: Example snippets for JavaDSL are subject to community contributions! Help us complete the docs, read more about it here: `write example snippets for Akka HTTP Java DSL #20466 <https://github.com/akka/akka/issues/20466>`_.
.. includecode:: ../../../../code/docs/http/javadsl/server/directives/SecurityDirectivesExamplesTest.java#authenticateBasicPF

View file

@ -22,4 +22,5 @@ See :ref:`credentials-and-timing-attacks-java` for details about verifying the s
Example
-------
TODO: Example snippets for JavaDSL are subject to community contributions! Help us complete the docs, read more about it here: `write example snippets for Akka HTTP Java DSL #20466 <https://github.com/akka/akka/issues/20466>`_.
.. includecode:: ../../../../code/docs/http/javadsl/server/directives/SecurityDirectivesExamplesTest.java#authenticateBasicPFAsync

View file

@ -16,4 +16,5 @@ More details about challenge-response authentication are available in the `RFC 2
Example
-------
TODO: Example snippets for JavaDSL are subject to community contributions! Help us complete the docs, read more about it here: `write example snippets for Akka HTTP Java DSL #20466 <https://github.com/akka/akka/issues/20466>`_.
.. includecode:: ../../../../code/docs/http/javadsl/server/directives/SecurityDirectivesExamplesTest.java#authenticateOrRejectWithChallenge

View file

@ -24,4 +24,5 @@ See also :ref:`-authorize-java-` for the asynchronous version of this directive.
Example
-------
TODO: Example snippets for JavaDSL are subject to community contributions! Help us complete the docs, read more about it here: `write example snippets for Akka HTTP Java DSL #20466 <https://github.com/akka/akka/issues/20466>`_.
.. includecode:: ../../../../code/docs/http/javadsl/server/directives/SecurityDirectivesExamplesTest.java#authorize

View file

@ -25,4 +25,5 @@ See also :ref:`-authorize-java-` for the synchronous version of this directive.
Example
-------
TODO: Example snippets for JavaDSL are subject to community contributions! Help us complete the docs, read more about it here: `write example snippets for Akka HTTP Java DSL #20466 <https://github.com/akka/akka/issues/20466>`_.
.. includecode:: ../../../../code/docs/http/javadsl/server/directives/SecurityDirectivesExamplesTest.java#authorizeAsync

View file

@ -13,4 +13,5 @@ See :ref:`credentials-and-timing-attacks-java` for details about verifying the s
Example
-------
TODO: Example snippets for JavaDSL are subject to community contributions! Help us complete the docs, read more about it here: `write example snippets for Akka HTTP Java DSL #20466 <https://github.com/akka/akka/issues/20466>`_.
.. includecode:: ../../../../code/docs/http/javadsl/server/directives/SecurityDirectivesExamplesTest.java#extractCredentials

View file

@ -6,6 +6,7 @@ package akka.http.javadsl.model;
import akka.Done;
import akka.stream.Materializer;
import akka.http.javadsl.model.headers.HttpCredentials;
import akka.util.ByteString;
import scala.concurrent.Future;
@ -113,6 +114,11 @@ public interface HttpMessage {
*/
Self addHeaders(Iterable<HttpHeader> headers);
/**
* Returns a copy of this message with the given http credential header added to the list of headers.
*/
Self addCredentials(HttpCredentials credentials);
/**
* Returns a copy of this message with all headers of the given name (case-insensitively) removed.
*/

View file

@ -109,6 +109,8 @@ sealed trait HttpMessage extends jm.HttpMessage {
def addHeader(header: jm.HttpHeader): Self = mapHeaders(_ :+ header.asInstanceOf[HttpHeader])
def addCredentials(credentials: jm.headers.HttpCredentials): Self = addHeader(jm.headers.Authorization.create(credentials))
/** Removes the header with the given name (case-insensitive) */
def removeHeader(headerName: String): Self = {
val lowerHeaderName = headerName.toRootLowerCase

View file

@ -12,9 +12,11 @@ import scala.compat.java8.FutureConverters._
import scala.compat.java8.OptionConverters._
import akka.http.javadsl.model.headers.HttpChallenge
import akka.http.javadsl.model.headers.HttpCredentials
import akka.http.javadsl.server.{ RequestContext, Route }
import akka.http.javadsl.server.{ Route, RequestContext }
import akka.http.scaladsl
import akka.http.scaladsl.server.{ AuthorizationFailedRejection, Directives D }
import akka.http.scaladsl.server.{ Directives D }
import scala.concurrent.{ ExecutionContextExecutor, Future }
object SecurityDirectives {
/**
@ -68,6 +70,50 @@ abstract class SecurityDirectives extends SchemeDirectives {
}
}
/**
* Wraps the inner route with Http Basic authentication support.
* The given authenticator determines whether the credentials in the request are valid
* and, if so, which user object to supply to the inner route.
*
* Authentication is required in this variant, i.e. the request is rejected if [authenticator] returns Optional.empty.
*/
def authenticateBasicPF[T](realm: String, authenticator: PartialFunction[Optional[ProvidedCredentials], T],
inner: JFunction[T, Route]): Route = RouteAdapter {
def pf: PartialFunction[scaladsl.server.directives.Credentials, Option[T]] = {
case c Option(authenticator.applyOrElse(toJava(c), (_: Any) null.asInstanceOf[T]))
}
D.authenticateBasic(realm, pf) { t
inner.apply(t).delegate
}
}
/**
* Wraps the inner route with Http Basic authentication support.
* The given authenticator determines whether the credentials in the request are valid
* and, if so, which user object to supply to the inner route.
*
* Authentication is required in this variant, i.e. the request is rejected if [authenticator] returns Optional.empty.
*/
def authenticateBasicPFAsync[T](realm: String, authenticator: PartialFunction[Optional[ProvidedCredentials], CompletionStage[T]],
inner: JFunction[T, Route]): Route = RouteAdapter {
def pf(implicit ec: ExecutionContextExecutor): PartialFunction[scaladsl.server.directives.Credentials, Future[Option[T]]] = {
case credentials
val jCredentials = toJava(credentials)
if (authenticator isDefinedAt jCredentials) {
authenticator(jCredentials).toScala.map(Some(_))
} else {
Future.successful(None)
}
}
D.extractExecutionContext { implicit ec
D.authenticateBasicAsync(realm, pf) { t
inner.apply(t).delegate
}
}
}
/**
* Wraps the inner route with Http Basic authentication support using a given `Authenticator[T]`.
* The given authenticator determines whether the credentials in the request are valid
@ -261,4 +307,4 @@ abstract class SecurityDirectives extends SchemeDirectives {
*/
def challengeFor(realm: String): HttpChallenge = HttpChallenge.create("Basic", realm)
}
}

View file

@ -886,6 +886,11 @@ object MiMa extends AutoPlugin {
// #20288 migrate BodyPartRenderer to GraphStage
ProblemFilters.exclude[IncompatibleResultTypeProblem]("akka.http.impl.engine.rendering.BodyPartRenderer.streamed")
),
"2.4.8" -> Seq(
// #20717 example snippet for akka http java dsl: SecurityDirectives
ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.javadsl.model.HttpMessage#MessageTransformations.addCredentials"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.scaladsl.model.HttpMessage.addCredentials")
)
)
}