Frage

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.

War es hilfreich?

Lösung

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.

Andere Tipps

The output from an action builder is an action, an action is essentially a function from request => future result, so you can actually just call it like this:

def tokenAndAuth = HasToken.async { implicit tokenRequest =>
  IsAuthenticated { implicit authRequest =>
    val token = tokenRequest
    val user = authRequest.user
    Ok("woho!")
  }(tokenRequest) // <-- call the inner action yourself, returns Future[SimpleResult]
}

There may be problems if there is a body that you want to parse though, I think it will be parsed by the body parser of the outer request but I'm not sure what the inner one will do if you would specify a parser.

The reason why you cannot do this in a simpler way is that you do not only want to compose logic but also pass data into each action, if you had actions that would just bail on missing auth or token you could absolutely compose them as the play docs on action composition describes.

Side note: since you are only looking at headers and not accessing the body in either of your custom action decorators it might be a better idea to look into EssentialAction:s as those would let you reject the request without first parsing the body when auth or token is missing.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top