Wednesday 4 September 2013

Building an assessment app in two days -- 8th commit

A few yawns are creeping in here, it's getting late…

The eighth commit is up, and now we can create courses. The interesting part of this commit, however is security.

Security in Assessory

If you have a look in CourseController, you'll see controllers that look like this:

/**
 * Retrieves a course
 */  
def get(id:String) = dataAction.one { 
  implicit request =>     
    val cache = request.approval.cache
    for (
      course <- cache(refCourse(id));
      approved <- request.approval ask 
                  Permissions.ViewCourse(course.itself)
    ) yield course
}

The permissions check is chained right there in the for loop (which is syntactic sugar for chaining flatMap calls on the Refs)

The way of thinking about it is that at any stage you can ask for approval to do something. That approval might be given; it might take some time to work out (involve looking something up in the database) and it might fail or be refused. All those fit neatly into the functionality of Ref, so we treat is asking for a Ref[Approved].

This also means it's independent of the database or Play classes, and I've declared the permission rules in the assessory-api module.

Security is in the API

If you look in Permissions, you can see the different permissions that an Approval[User] can ask to be approved.

Sometimes these are straightforward objects:

case object CreateCourse extends Perm[User] {    
  def resolve(prior:Approval[User]) = {
    Approved("Anyone may create a course")
  }
}

And sometimes they are approvals on an item:

  case class ViewCourse(course:Ref[Course]) 
       extends PermOnIdRef[User, Course](course) 
  {
    def resolve(prior:Approval[User]) = 
      hasRole(
         course, prior.who, 
         CourseRole.student, prior.cache
      )
  }

Approvals on an item (PermOnIdRef) are clever enough to realise that if you ask for an approval on Course(id=1).itself, and you ask for an approval on LazyId(classOf[Course], "1"), those are the same approval and it doesn't need to look up the ID the second time.

And it can do that independently of what kind of database you've wired up, or whether or not it's in a Play app.

Cache

As well as remembering granted approvals, Approval also contains a cache for Ref lookups.

The Approval tends to be present in all three of the controller, the security check, and the JSON conversion -- and these are the three places where you typically need to look up Refs. So having a cache attached to it is rather handy.

JSON conversion is seemless with security too

All this flatMapping on Refs has another payoff in the JSON conversion.

If you have a look at CourseToJSON, you'll see that it embeds a permissions block into the JSON it returns

def toJsonFor(c:Course, a:Approval[User]) = {

  val permissions = for (
    view <- optionally(
      a ask Permissions.EditCourse(c.itself)
    );
    edit <- optionally(
      a ask Permissions.EditCourse(c.itself)
    )
  ) yield Json.obj(
    "view" -> view.isDefined,
    "edit" -> edit.isDefined
  )

  for (p <- permissions) yield {
    courseFormat.writes(c) ++ 
    Json.obj("permissions" -> p)
  }
}

This happens asynchronously -- the user reference in the approval is asynchronous and may or may not already have been retrieved.

The JSON block that this produces ends up looking like something this:

{
  "id":"52275e6a9acb3a4500d7c2ea",
  "title":"Design Computing Studio 2",
  "shortName":"DECO2800",
  "shortDescription":"…",
  "addedBy":"522732099acb3a4d00fedff9",
  "created":1378311786259,
  "permissions": {
    "view":true,
    "edit":true
  }
}

Because the permissions block is in the item, on the client it is easy to enable and disable components using Angular.js.

Say, for instance, we might have an edit link that only shows if the edit permission is present:

<a href="edit" ng-show="course.permissions.edit">
  Edit
</a>

No comments: