Coffee. On the rocks!

A few more learnings with Play Framework

Thu 02 July 2015

In this post i will write about some problems i solved with Play Framework. Considet the case of an internal application of a publishing company where for every request you need to authenticate the employee. Also every employee has one or more permissions like publish an article, edit an article, read an artice, create a new video, etc. So after i authenticate the employee(verify that he is who he says he is) i also need to authorize him(check if he can take an action). For this i used Play's action composition. First lets take care of authentication. The naive way of authenticating a request is

def someAction = Action.async { implicit request =>
    // now i have access to the request so i can autheticate it here
    if (authenticated(request)) {
        // process request further
    } else
        Future.successful(Unauthorized)
}

But if do this then for every action that needs authentication i have to call the function authenticated. What if i have a type of authenticated Action which i can use straightaway? What is have a type of authenticated Request that provides me with the employee data who is making the request? I need to transform a Request to an AuthenticatedRequest if the request has the correct values for authentication params(Read correct id/password combo). The authenticated request would have the employee id apart from the request data. For this i'll use Play's WrappedRequest

class AuthenticatedEmployeeRequest[T](val employeeId: Int, val request: Request[T]) extends WrappedRequest[T](request)

Now i need to build the AuthenticatedAction. For this i use Play's ActionBuilder which is used to build actions. Also i will need Play's ActionRefiner which would convert a normal Play Request to an AuthenticatedRequest

object AuthenticatedAction extends ActionBuilder[AuthenticatedEmployeeRequest] with ActionRefiner[Request, AuthenticatedEmployeeRequest] {

    def refine[T](request: Request[T]): Future[Either[Result, AuthenticatedEmployeeRequest[T]]] = {
      getParams(request.headers) match {
        case Some(authParams: (String, String)) =>
          EmployeeData.authenticate(authParams._1.toInt, authParams._2).flatMap {
            case r: Boolean if r =>
              Future(Right(new AuthenticatedEmployeeRequest[T](authParams._1.toInt, request)))
            case _ =>
              Future(Left(Forbidden("Auth token does not match")))
          }
        case _ =>
          Future(Left(Forbidden("No auth params sent")))
      }
    }
}

The refine method of the ActionRefiner takes a normal Play Request and return Future of an Either. It extracts auth params from it and tries to validate them. If they are good then it returns a Future of Right of AuthenticatedEmployeeRequest otherwise it returns a Future of Left of a Play result which will be Forbidden in this case because the auth params do not match for any employee. Now this authenticated action can be used like any other action, eg

def authenticatedArea = AuthenticatedAction.async { implicit request =>
    // the request here is not a normal Play Request but an AuthenticatedEmployeeRequest. Request data like queryString can be accessed as request.request.queryString. request.employeeId would be the id of the employee whose credentials are used to perform that action.
}

Thats okay but what about permissions. Lets say i have an there is an edit article request that can only be allowed if the employee has both readArticle and writeArticle permissions. Also there is publish article request that can only be allowed if the employee has publishArticle, readArticle and writeArticle permissions. There is article statistics request that can only be allowed if the employee has readArticle and articleStats permissions. And many more requests which need some combination of permissions. Now clearly i need some way in which i can generate an action by passing a sequence of permissions. That action would reject any request where the employee making the request does not have all the permissions it requires. Also i would need any request to these authorized actions to be authenticated also. So lets build on our previous work.

Like i had an AuthenticatedEmployeeRequest which had employee id, i would now like to have a AuthorizedEmployeeRequest which would have the employee id and its permissions also. Again i create a WrappedRequest.

class AuthorizedEmployeeRequest[T](val employeeId: Int, val permissions: Map[String, Boolean], val request: AuthenticatedEmployeeRequest[T]) extends WrappedRequest[T](request)

Now i create a method which takes a sequence of permissions and returns an action. Again i use ActionRefiner which will convert an AuthenticatedEmployeeRequest to an AuthorizedEmployeeRequest. Here is how

def AuthorizedEmployeeAction(requiredPermissions: Seq[String] = Seq.empty) = new ActionRefiner[AuthenticatedEmployeeRequest, AuthorizedEmployeeRequest] {
    def refine[T](request: AuthenticatedEmployeeRequest[T]) = {
      if (EmployeeData.hasPermissions(requiredPermissions))
        Right(new AuthorizedEmployeeRequest[T](request.employeeId, companyId, allEmployeePermissions, request))
      else
        Left(Forbidden("Not permitted"))
    }
}

Finally i would compose the AuthenticatedAction and AuthorizedEmployeeAction.

def AuthEmployeeAction(requiredPermissions: Seq[String] = Seq.empty) = {
    AuthenticatedAction andThen AuthorizedEmployeeAction(requiredPermissions)
}

Now i can have actions like

def read = AuthEmployeeAction(Seq("readArticle")).async { implicit request =>
    // employee needs only readArticle permission to successfully make this request
}

def edit = AuthEmployeeAction(Seq("readArticle", "writeArticle")).async { implicit request =>
    // employee needs both readArticle and writeArticle permissions to successfully make this request
}

def publish = AuthEmployeeAction(Seq("readArticle", "writeArticle", "publishArticle")).async { implicit request =>
    // employee needs readArticle, writeArticle and publishArticle permissions to successfully make this request
}

Okay, thats all about auth. One more problem is data validation while form posting(Read POST requests). A trivial post request would be handled like this

case class UserFormData(name: String,
                        locationName: Option[String],
                        age: Option[Int]
                         )

val userDataMapping = mapping(
    "name" -> nonEmptyText,
    "locationName"-> optional(text),
    "age"-> optional(number(min=1))
  )(UserFormData.apply)(UserFormData.unapply)


def userForm  = Form(userDataMapping)

def createUser = AuthEmployeeAction(Seq("readUser", "writeUser")).async { implicit request =>
    userForm.bindFromRequest.fold(
        formWithErrors => Future.successful(BadRequest(formWithErrors.errorsAsJson)),
        validForm => {
            // now use validForm.name, validForm.age, etc
        }
    )
}

But how about POST requests where one of the field is a URL, some fields are in required to be in JSON format, some fields are in some other cusotm format like comma separated integers or location in lat,lang format. For this i have to define custom form mappings and validations. Play allows to define custom constraints. Lets write a URL validation constraint

val validUrlConstraint: Constraint[String] = Constraint("constraints.validurl")({
    plainText =>
      try {
        new URL(plainText)
        Valid
      }
      catch {
        case e: Exception =>
          Invalid(Seq(ValidationError(e.getMessage), ValidationError("text is not url")))
      }
})

This is pretty simple. You pass the data through your validation logic and if it succeeds you return Valid else return InValid. Lets write a constraint for validating JSON.

val validJsonConstraint: Constraint[String] = Constraint("constraints.validjson")({
    plainText =>
      try {
        Json.parse(plainText)
        Valid
      }
      catch {
        case e: Exception =>
          Invalid(Seq(ValidationError(e.getMessage), ValidationError("text is not json")))
      }
})

Also a location data constraint. I consider valid location data only that which has 2 integers separated by a colon.

val validLocationConstraint: Constraint[String] = Constraint("constraints.validlocation")({
    plainText =>
      val pattern = new Regex("^\\d{1,3}:\\d{1,3}$")
      val matches = pattern.findFirstMatchIn(plainText) match {
        case Some(m: Match) =>
          // do some more processing if needed and return true or false accordingly
        case _ => false
      }
      if (matches)
        Valid
      else
        Invalid(Seq(ValidationError("location is not properly formatted")))
})

Now i need the form mapping similar to Play's number, email, etc. Something which would take the text in the request's field and convert it to a type. Here is how i write a URL mapping

val url: Mapping[URL] = of[String].verifying(validUrlConstraint).transform(
    (s: String) => new URL(s),
    (u: URL) => u.toString
)

In the mapping above i first validate whether the text can be converted to a URL and if it can be then i define how it can be tranformed to a URL and vice versa. Similarly a mapping for json

val json: Mapping[JsValue] = of[String].verifying(validJsonConstraint).transform(
    (s: String) => Json.parse(s),
    (j: JsValue) => j.toString()
)

For comma separated integers

val csi: Mapping[List[Int]] = of[String].verifying(Constraints.nonEmpty).transform(
    (s: String) => s.split(",").map(_.toInt).toList,
    (s: List[Int]) => s.mkString(",")
)

An example of using these custom form mappings

case class ArticleFormData(name: String,
                        primaryUrl: URL,
                        secondaryUrl: Option[URL],
                        foramtting: JsValue,
                        tagIds: List[Int]
                         )

val articleDataMapping = mapping(
    "name" -> nonEmptyText,
    "primaryUrl"-> url,
    "secondaryUrl"-> optional(url),
    "foramtting"-> json,
    "tagIds"-> csi
  )(ArticleFormData.apply)(ArticleFormData.unapply)


def articleForm  = Form(articleDataMapping)

def createArticle = AuthEmployeeAction(Seq("readArticle", "writeArticle")).async { implicit request =>
    articleForm.bindFromRequest.fold(
        formWithErrors => Future.successful(BadRequest(formWithErrors.errorsAsJson)),
        validForm => {
            validForm.primaryUrl.isInstanceOf[URL]                // is true
            validForm.secondaryUrl.isInstanceOf[Option[URL]]      // is true
            validForm.tagIds.isInstanceOf[List[Int]]              // is true

            // create article object
        }
    )
}

Now my approach to error handling in Play(Do tell if you see some problems with this). A lot of times i encounter errors in my application which i would like to report to the frontend with some error code and error message. Like the employee tries to change the slug of the article to something which is already the slug of another article. In this case i would like to return an error to the frontend. Or the employee is trying to link some article to another article but that cannot be done for some regulatory reasons. Again tell the employee/frontend the error code and message. The problem with this is that such errors are not encounntered in the controller but in the model's method which was called by the controller or maybe some method which was called by the model's method which in turn was called by the controller. So i have to pass these exceptions all the way up to the controller to generate an error message. Also i sometimes forget to catch these exceptions and they just blow up in my face(Scala does not have checked exceptions). So wouldnt it be nice if any of those exceptions managed to reach to the controller they would result in pretty messages for the frontend rather than a long stacktrace. Or better, how could i stop caring about those exceptions altogether and just throw them and be assured that they would result in pretty error messages.

trait AppException extends Exception {

  val success = false
  def errorCode: Int
  def message: String
  val resultStatus: Results.Status = Results.InternalServerError

  override def fillInStackTrace(): Throwable = {        // overiding to save the cost of generating the stacktrace. must do.
    this
  }
}

object NotAuthorizedToReadObject extends AppException {
  val errorCode = 4001
  val message = "You dont have permissions to access that object"
  override val resultStatus = Results.Unauthorized
}

object NotAuthorizedToWriteObject extends AppException {
  val errorCode = 4002
  val message = "You dont have permissions to access that object"
  override val resultStatus = Results.Unauthorized
}

object EmployeeNotFound extends AppException {
  val errorCode = 5008
  val message = "Employee with given id not found"
}

object EmployeeWithEmailExists extends AppException {
  val errorCode = 5009
  val message = "Employee with email exists"
}

Last thing which i need to do is overide the global onError method. Play has a GlobalSettings trait which provides methods like onStart, onStop, etc which can be overriden to create hooks. Here i will overirde onError method which is called when Play encounters any exception. In onError i check if the exception is of type AppException then send a generated response from that exception object otherwise do what Play does in case of an exception.

object Global extends GlobalSettings {
    override def onError(request: RequestHeader, ex: Throwable) = {
        ex.getCause match {
          case c: AppException =>
            Future.successful(c.resultStatus(
              Json.toJson(Json.obj(
                "success"-> c.success,
                "message"-> c.message,
                "errorCode"-> c.errorCode
              ))
            ))
          case _ =>
            super.onError(request, ex)
        }
    }
}

So from now any Exception extending the trait AppException can be just thrown without caring. Just write throw EmployeeNotFound in a controller or anywhere else and the frontend would get a json reponse and not a stacktrace.

Thats it for now. Would love to get some feedback on this.

This entry was tagged as playframework

blog comments powered by Disqus