Composite constraints

It is possible to create composite constraints by creating a tree of the existing constraint types.

The available constraint types are

  • subject present
  • subject not present
  • restrict
  • pattern
  • dynamic
  • constraint tree

The first five are exactly as covered elsewhere in this documentation; a constraint tree contains a list of constraints along with the AND/OR operator that should be applied to them. Because ConstraintTree is also a Constraint, sub-trees can be used to form arbitrarily complex constraints.

Constraints can be specified in an ad-hoc manner; in this example, access to index requires that either no subject is present or a subject is present that holds the killer.undead.zombie permission.

class Composite @Inject()(deadbolt: DeadboltActions,
                          constraints: CompositeConstraints,
                          ecProvider: ExecutionContextProvider) extends Controller {

  val ec: ExecutionContext = ecProvider.get()

  def index = deadbolt.Composite(
       constraint = constraints.ConstraintTree[A](||(ec),
                                                  List(constraints.SubjectNotPresent(),
                                                       constraints.Pattern("killer.undead.zombie",
                                                                           PatternType.REGEX))))() { authRequest =>
      Future {
        Ok("Content accessible")
      }
}

In the interests of re-use and testing, as well as keeping your controllers readable, you can define your constraints centrally and reference them in your controller layer. The easiest way to do this is to use an eager singleton to define the constraints.

@Singleton
class MyCompositeConstraints @Inject() (constraints: CompositeConstraints,
                                        ecProvider: ExecutionContextProvider){

  val ec: ExecutionContext = ecProvider.get()

  def zombieKillerOrNoSubjectPresent[A](): Constraint[A] = 
       constraints.ConstraintTree[A](||(ec),
                                     List(constraints.SubjectNotPresent(),
                                          constraints.Pattern("killer.undead.zombie",
                                                              PatternType.REGEX)))
}

Once registered with the DI system, these constraints can then be injected into controller; re-writing the controller above, a lot of complexity disappears.

class Composite @Inject()(deadbolt: DeadboltActions,
                          compositeConstraints: MyCompositeConstraints) extends Controller {

  val ec: ExecutionContext = ecProvider.get()

  def index =
    deadbolt.Composite(constraint = compositeConstraints.zombieKillerOrNoSubjectPresent())() { authRequest =>
      Future {
        Ok("Content accessible")
      }
}