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 Ref
s)
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 Ref
s. So having a cache attached to it is rather handy.
JSON conversion is seemless with security too
All this flatMapping on Ref
s 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:
Post a Comment