6
votes

Briefly: my application uses the Play web framework version 2.5.1. I want to use the Deadbolt authorization system, and Slick to access the user-authorization information in my database. How can I do this? Deadbolt is made specifically for Play, and Play comes with Slick integrated out-of-the-box, so it ought to be possible if not very easy.

Based on "Integrating Deadbolt" from the Deadbolt documentation, I extended the DeadboltHandler trait. Its abstract getSubject() method seems like the place to do the database query (so says the documentation but without any example). That method receives as an argument an AuthenticatedRequest and returns the Subject, basically the user-id that was authenticated, along with roles and permissions (authorizations).

I am stuck, because while Play comes with Slick integration, the documentation describes only how to use it from within a Play controller. (Note I am wanting to do this using dependency injection since using global lookups is deprecated and error-prone)

I am successfully using Deadbolt in my controller to restrict access to certain resources, but the controller seems like the wrong place for Deadbolt to be doing database queries for authorization details (if it were, then the DeadboltHandler would be purposeless). The controller constructor signature definition looks something like (note the controller accesses the default database that stores web content rather than the authorization database):

class Application @Inject()(
  dbConfigProvider: DatabaseConfigProvider,
  playConfig: play.api.Configuration,
  deadbolt: DeadboltActions
) extends Controller {

That works. However, similarly annotating the DeadboltHandler extension with @Inject fails to provide Slick access to the database:

class AuthHandler @Inject()(@play.db.NamedDatabase("auth") dbConfigProvider: DatabaseConfigProvider)
  extends DeadboltHandler {

the result being

not enough arguments for constructor AuthHandler: (dbConfigProvider: play.api.db.slick.DatabaseConfigProvider)services.AuthHandler.
Unspecified value parameter dbConfigProvider.

Obviously, Play does something special for controllers so that the @Inject annotation works, something of which I lack understanding. I presume it is in the nature of constructing controllers using an injector rather than the new keyword, but my search through the Play source code has failed to show me what exactly is happening. If I could find that, perhaps I could mimic that technique to construct a DeadboltHandler.

I see that play comes with classes such as GuiceInjector and GuiceInjectorBuilder, which sound as if they might be part of the solution, but my experimentation has yet failed to show me how, and if there is any documentation on how to use them in the specific context of a DeadboldHandler extension, I am missing it.

I found this previous question: Scala (Play 2.4.x) How to call a class with @inject() annotation, which seems very much on point. Unfortunately, despite a half-dozen follow-up comments from the original poster, it is yet unanswered. I feel if I had the answer to that question I would have the answer to this question, though my question is very specific: how to use Play and Deadbolt and Slick with each other (in Scala).

What baffles me most is that this seems like something that ought to be common enough that it would be either mentioned in the documentation or have been asked about already on SO. My failure to find any such references typically means I am doing something so uniquely wrong that nobody else has ever had occasion to talk about it. It certainly seems as if it ought to be simple enough that I am optimistically hoping that I am missing something very basic, and I look forward to some kind soul informing me of that knowledge.

1
I'll write up a full answer later, but for now you can take a look at github.com/schaloner/deadbolt-auth0-scala/blob/master/app/… and github.com/schaloner/deadbolt-auth0-scala/blob/master/app/… - this example uses an external identity management platform in place of a database, but you should be able to rewrite github.com/schaloner/deadbolt-auth0-scala/blob/master/app/… and use most of the code as-is.Steve Chaloner
You unstuck me, bless you Steve! Take as long as you need to write up a full answer; you have a correct-answer mark waiting for you.Adam Mackler

1 Answers

2
votes

As you noted in your question, the place to retrieve the user is in DeadboltHandler.getSubject. You can actually move the database-specific code into its own class, so in this example that's what I've done.

This is a generic implementation of DeadboltHandler; you should be able to drop it into your code and use it pretty much as-is, as the persistence specifics will be dealt with later.

import javax.inject.{Inject, Singleton}

import be.objectify.deadbolt.scala.models.Subject
import be.objectify.deadbolt.scala.{AuthenticatedRequest, DeadboltHandler, DynamicResourceHandler}
import models.{LogInForm, User}
import play.api.mvc.{Request, Result, Results}
import play.twirl.api.HtmlFormat
import views.html.security.denied

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

@Singleton
class MyDeadboltHandler @Inject() (authSupport: AuthSupport) extends DeadboltHandler {

  override def beforeAuthCheck[A](request: Request[A]): Future[Option[Result]] = Future {None}

  override def getDynamicResourceHandler[A](request: Request[A]): Future[Option[DynamicResourceHandler]] = Future {None}

  /**
    * Get the current user.
    *
    * @param request the HTTP request
    * @return a future for an option maybe containing the subject
    */
  override def getSubject[A](request: AuthenticatedRequest[A]): Future[Option[Subject]] = 
    Future {
      request.subject.orElse {
        // replace request.session.get("userId") with how you identify the user
        request.session.get("userId") match {
          case Some(userId) => authSupport.getUser(userId)
          case _ => None
        }
      }}

  /**
    * Handle instances of authorization failure.
    *
    * @param request the HTTP request
    * @return either a 401 or 403 response, depending on the situation
    */
  override def onAuthFailure[A](request: AuthenticatedRequest[A]): Future[Result] = {
    def toContent(maybeSubject: Option[Subject]): (Boolean, HtmlFormat.Appendable) =
      maybeSubject.map(subject => subject.asInstanceOf[User])
      .map(user => (true, denied(Some(user))))
      .getOrElse {(false, views.html.security.logIn(LogInForm.logInForm))}

    getSubject(request).map(maybeSubject => toContent(maybeSubject))
    .map(subjectPresentAndContent =>
      if (subjectPresentAndContent._1) Results.Forbidden(subjectPresentAndContent._2)
      else Results.Unauthorized(subjectPresentAndContent._2))
  }
}

The need to go to the database is now reduced to cases where the subject has not already been placed into the request. Note the comment about replacing request.session.get("userId") with however you identify the user.

Access to subject persistence is then provided by the AuthSupport class. This isolates DB access from the DeadboltHandler. It's pretty simple, mainly because you'll be filling this in with your Slick query.

@Singleton
class AuthSupport @Inject()(dbConfigProvider: DatabaseConfigProvider) {
    // set up your usual Slick support

    // use Slick to get the subject from the database
    def getUser(userId: String): Option[User] = ???
}

To expose this, you'll need to create a module and register it in your application.conf.

import be.objectify.deadbolt.scala.DeadboltHandler
import be.objectify.deadbolt.scala.cache.HandlerCache
import security.{AuthSupport, MyDeadboltHandler, MyHandlerCache}
import play.api.inject.{Binding, Module}
import play.api.{Configuration, Environment}

class CustomBindings extends Module  {
  override def bindings(environment: Environment,
                        configuration: Configuration): Seq[Binding[_]] =
    Seq(
         bind[DeadboltHandler].to[MyDeadboltHandler],
         bind[AuthSupport].toSelf,
         // other bindings, such as HandlerCache
       )
}

Declaring it in application.conf is the usual matter of using play.modules.enabled:

play {
  modules {
    enabled += be.objectify.deadbolt.scala.DeadboltModule
    enabled += modules.CustomBindings
  }
}