I am using Play's ActionBuilder to create various Actions that secure my controllers. For instance, I implemented IsAuthenticated
to make sure that certain actions can only be accessed if the user would be logged in:
case class AuthRequest[A](user: String, request: Request[A]) extends WrappedRequest[A](request)
private[controllers] object IsAuthenticated extends ActionBuilder[AuthRequest] {
def invokeBlock[A](req: Request[A], block: (AuthRequest[A]) => Future[SimpleResult]) = {
req.session.get("user").map { user =>
block(new AuthRequest(user, req))
} getOrElse {
Future.successful(Results.Unauthorized("401 No user\n"))
}
}
}
Using IsAuthenticated
I can (1) restrict an action to users who are logged in, and (b) access the user being logged in:
def auth = IsAuthenticated { implicit authRequest =>
val user = authRequest.user
Ok(user)
}
Furthermore, I use ActionBuilder HasToken
to ensure that an action was invoked with a token being present in the request's header (and, I can access the token value):
case class TokenRequest[A](token: String, request: Request[A]) extends WrappedRequest[A](request)
private[controllers] object HasToken extends ActionBuilder[TokenRequest] {
def invokeBlock[A](request: Request[A], block: (TokenRequest[A]) => Future[SimpleResult]) = {
request.headers.get("X-TOKEN") map { token =>
block(TokenRequest(token, request))
} getOrElse {
Future.successful(Results.Unauthorized("401 No Security Token\n"))
}
}
}
That way, I can make sure that an action was called with that token present:
def token = HasToken { implicit tokeRequest =>
val token = tokeRequest.token
Ok(token)
}
So far, so good...
But, how could I wrap (or, nest / compose) such actions as those defined above? For instance, I would like to ensure (a) that a user would be logged in and (b) that the token would be present:
def tokenAndAuth = HasToken { implicit tokeRequest =>
IsAuthenticated { implicit authRequest =>
val token = tokeRequest.token
val user = authRequest.user
}
}
However, the above action does not compile. I tried many different implementations but always failed to achieve the desired behavior.
In general terms: How could I compose Action
s defined using Play's ActionBuilder
in arbitrary order? In the above example, it would not matter if I would wrap IsAuthenticated
in HasToken
or the other way around -- the effect would be the same: the user would have to be logged in and would have to present the token.
Note: I have created a Gist that provides the complete source code.
ActionBuilders are not made for ad-hoc composition, but rather to build a hierarchy of actions so you end up using only a couple of actions throughout your controllers.
So in your example you should build IsAuthenticated
on top of HasToken
as I illustrated here.
This is a viable solution and can actually simplify your code. How often do you really need to compose on the spot?
Ad-hoc composition could be achieved with EssentialActions (simply because they haven't changed from 2.1), but they have a few downsides, as Johan pointed out. Their API is not really intended for ad-hoc use either, and Iteratees are too low-level and too cumbersome for controller actions.
So finally your last option would be to write Actions directly. Actions do not support passing a WrappedRequest by default (that's why ActionBuilder exists). However you can still pass a WrappedRequest and have the next Action deal with it.
The following is the best I have come up with so far and is rather fragile I guess.
case class HasToken[A](action: Action[A]) extends Action[A] {
def apply(request: Request[A]): Future[SimpleResult] = {
request.headers.get("X-TOKEN") map { token =>
action(TokenRequest(token, request))
} getOrElse {
Future.successful(Results.Unauthorized("401 No Security Token\n"))
}
}
lazy val parser = action.parser
}
case class IsAuthenticated[A](action: Action[A]) extends Action[A] {
def apply(request: Request[A]): Future[SimpleResult] = {
request.session.get("user").map { user =>
action(new AuthRequest(user, request))
} getOrElse {
Future.successful(Results.Unauthorized("401 No user\n"))
}
}
lazy val parser = action.parser
}
object ActionComposition extends Controller {
def myAction = HasToken {
Action.async(parse.empty) { case TokenRequest(token, request) =>
Future {
Ok(token)
}
}
}
def myOtherAction = IsAuthenticated {
Action(parse.json) { case AuthRequest(user, request) =>
Ok
}
}
def both = HasToken {
IsAuthenticated {
Action(parse.empty) { case AuthRequest(user, request: TokenRequest[_]) =>
Ok(request.token)
}
}
}
}
You can also compose at the Result level and only use the built-in actions. This is especially useful when trying to factor out error handling and other repetitive stuff. I have an example here.
We are still missing the capabilities that Play 2.1's action composition offered. So far to me it seems that ActionBuilder + Result composition is the winner as its successor.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With