Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to wrap Actions (in any order) when using Play's ActionBuilder?

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 Actions 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.

like image 711
Martin Avatar asked Feb 10 '14 13:02

Martin


1 Answers

ActionBuilder

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?

EssentialAction

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.

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)
      }
    }
  }
}

Results

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.

Conclusion

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.

like image 66
Marius Soutier Avatar answered Nov 05 '22 07:11

Marius Soutier