scalaplayframeworksecuresocial

Extend SecureActionBuilder to Validate Request Before Parsing the Body


I am working on writing a web application that takes multipart files as input and uploads them to an S3 instance. Since some of the files can be very large, I am using a custom Body Parser to send the chunks to S3 as they come in.

I want to do validation on the request prior to uploading the file (to check that the user has permission/enough space, etc). From reading the Play docs, it seems like extending ActionBuilder is the right way to do this. I noticed in SecureSocial, there is a SecureActionBuilder and I believe I should extend this in order to build a secure action (which is what I want).

I tried this simple test to see if I could print out the userId, therefore being able to perform actions based on the user.

object FileValidationAction extends SecuredActionBuilder {
  def invokeBlock[A](request: SecuredRequest[A], block: SecuredRequest[A] => Future[SimpleResult]) = {
    Logger.info("User id is " + request.user.userProfile.userId)
    block(request)
  }
}

However, the method never got called.

Next, I tried overriding the method from the SecuredActionBuilder object:

object FileValidationAction extends SecuredActionBuilder {
  override def invokeBlock[A](request: Request[A], block: SecuredRequest[A] => Future[SimpleResult]) = {
    val securedResult: Option[SecuredRequest[A]] = request match {
      case r: SecuredRequest[A] => Option(r)
      case _ => None
    }
    Logger.info("Calling action ------- WOO!")
    securedResult match {
      case Some(r) =>
        block(r)
      case _ =>
        Future.successful(Forbidden)
    }
  }
}

That method gets called but the request coming in is not a SecuredRequest as I was hoping.

How do I build a SecuredAction, using a custom body parser, that I can do validation on before it completes (or even starts) the upload?

EDIT:

To clarify, I will be calling the Action with the following method signature:

def upload = FileValidationAction(streamingBodyParser(streamConstructor)) { request =>

Solution

  • The problem is that you are not invoking the original code in SecuredActionBuilder that actually checks if the user is there and constructs the SecuredRequest instance.

    Something like this should work:

    // A sample usage of the action
    def checkFile = FileValidationAction { request =>
        Ok("")
      }
    
    // The builder implementation
      object FileValidationAction extends FileValidationActionBuilder {
        def apply[A]() = new FileValidationActionBuilder()
      }
    
      class FileValidationActionBuilder(authorize: Option[Authorization[DemoUser]] = None) extends SecuredActionBuilder(authorize) {
        def validateFile[A](block: SecuredRequest[A] => Future[SimpleResult]): SecuredRequest[A] => Future[SimpleResult] = { securedRequest =>
          Logger.info(s"User id is ${securedRequest.user.main.userId}")
          block(securedRequest)
        }
    
        override def invokeBlock[A](request: Request[A], block: (SecuredRequest[A]) => Future[SimpleResult]): Future[SimpleResult] = {
          invokeSecuredBlock(authorize, request, validateFile(block))
        }
      }
    

    You would need to add this inside the controller where you plan to use this actions. If you need to use it in multiple controllers then create a trait that you can extend with this.

    Also note that in this sample code I am using the DemoUser type I have in the samples. You will need to change that to the type you are using in your app to represent users.