Managing fine-grained mongoDB object access in Scala

November 04, 2013 • hack

Restricting db access to certain collections or even specific objects is a major requirement for a lot of software projects. For us the access control should

  1. not get in our way, we don’t want to get bothered with DAC in every query
  2. be implemented in a way that every Data Access Object (DAO) can use it
  3. be easy to turn on and off on a DAO basis

There are two common ways to manage DB object based access control:

  • request objects and verify access rights in business logic AFTER reading it from the DB
  • embed security restrictions into the query and apply them WHILE querying

The first approach creates DB requests regardless of a users access rights. This might create a lot of useless DB traffic. We decided to go for the last as the filtering is done on the DB side which in most cases is more performant.

Target system overview

Query based Data access control (DAC)

Imagine the following scenario: You have multiple teams and each team has its own blog with several posts. There are public posts which can be viewed by everyone and private post which are only accessible by members of the team.

The goal is to be able to query the db in a way like this:

def findMostRecentPosts()(implicit ctx: DBAccessContext) = {
  find().sortBy("createdAt" -> Desc).limit(5)
}

def findByTitle(title: String)(implicit ctx: DBAccessContext) = {
  find(Json.obj("title" -> title)).one
}

Where ctx contains the querying user and restricts the queries to the allowed posts. A post could look like this:

{
  "team" : "superheros",
  "title" : "Flying through the sky",
  "content" : "Once upon a time...",
  "createdAt" : "1383614577",
  "isPublic" : true,
  "isDeleted" : false
}

Database access context

sealed trait DBAccessContext {
  def determineAccess(f: Option[User] => AccessRestriction): AccessRestriction
}

case class AuthedAccessContext(user: User) extends DBAccessContext {
  // if there is a user available
  def determineAccess(f: Option[User] => AccessRestriction) = f(Some(user))
}

case object UnAuthedAccessContext extends DBAccessContext {
  // if there is no user available
  def determineAccess(f: Option[User] => AccessRestriction) = f(None)
}

case object GlobalAccessContext extends DBAccessContext {
  // allows every DB action, can be imported into scope
  def determineAccess(f: Option[User] => AccessRestriction) = Allow
}

The function that needs to be passed to determineAccess is specific to the collection and operation. Therefore each DAO can specify in what way access to an operation should be granted.

An AccessRestriction restricts the query to be made.

sealed trait AccessRestriction {
  // defines what needs to be appended to the query json
  def appendToQuery: JsObject = Json.obj()

  // defines if the query is allowed
  def isAllowed: Boolean
}

case object Deny extends AccessRestriction {
  // deny a specific action
  def isAllowed = false
}

case object Allow extends AccessRestriction {
  // allow a specific action
  def isAllowed = true
}

case class AllowIf(override val appendToQuery: JsObject) extends AccessRestriction {
  // allow a specific action, but append to query to restrict it
  def isAllowed = true
}

In case of the above example the determineAccess should result in something like AllowIf(Json.obj("team" -> "Badguys")).

Base DAO

To use the above DBAccessContext, all database calls must be proxies by a SecuredCollection which makes sure that all constraints are maintained.

trait SecuredCollection{
  val underlying: JSONCollection

  //...
  def findQueryFilter(userOpt: Option[User]): AccessRestriction

  def find(query: JsObject = Json.obj())(implicit ctx: DBAccessContext) = {
    val access = ctx.determineAccess(findQueryFilter)
    if (access.isAllowed)
      underlying.find(query ++ access.appendToQuery)
    else
      AccessDeniedError
  }
  //...
}

Specific DAO for access to posts

The DAO for our posts collection can now use this SecuredCollection to ensure access privileges.

object BlogPostDAO extends SecuredCollection with DAOHelpers{
  lazy val underlying = Mongo.collection("posts")

  // filter which is used to restrict find queries. The returned Json is appended
  // to each query of the user
  def findQueryFilter(userOpt: Option[User]) = userOpt match {
    case Some(user) =>
      AllowIf(Json.obj(
        "$or" -> List(
          Json.obj("team" -> user.team),
          Json.obj("isPublic" -> true))))
    case _ =>
      AllowIf(Json.obj("isPublic" -> true))
  }

  def findMostRecentPosts()(implicit ctx: DBAccessContext) = {
    find().sort(Json.obj("createdAt" -> Desc)).cursor[JsObject].collect[List](5)
  }

  def findByTitle(title: String)(implicit ctx: DBAccessContext) = {
    find(Json.obj("title" -> title)).one[JsObject]
  }
}

The awesome thing is, that you only need to define the restriction once. It then gets picked up in every request. There is an awesome side effect of this approach: It is possible to restrict all queries, independent of the user. This can be used to implement a “delete nothing” strategy where posts are marked as being deleted but actually still kept in the DB. The following filter would filter out all deleted posts:

  def findQueryFilter(userOpt: Option[User]) = userOpt match {
    case Some(user) =>
      AllowIf(Json.obj(
        "$or" -> List(
          Json.obj("team" -> user.team),
          Json.obj("isPublic" -> true)),
        "isDeleted" -> false))
    case _ =>
      AllowIf(Json.obj("isDeleted" -> false, "isPublic" -> true))
  }

There is only one thing missing to get this running.

Create a DBAccessContext

The best way is to create an implicit conversion to automatically create an DBAccessContext in to use DB calls from controllers (this is Play 2.0 specific).

  implicit def userToDBAccess[T](implicit request: AuthenticatedRequest[T, User]) =
    AuthedAccessContext(request.user)

(Request authentication: Play ScalaActionComposition)

In your controllers you can now access DB methods in a secured way:

object PostController extends Controller{
  def mostRecent = Authenticated.async{ implicit request =>
    BlogPostDAO.findMostRecentPosts.map{ posts =>
      Ok(Json.toJson(posts))
    }
  }
}

Wrap-up

Advantages

  • fine grained access control on a per object basis
  • “All-Access-Mode” for classes to be able to query all objects using GlobalAccessContext

Disadvantages

  • everything or nothing, it is not possible to allow partial access to objects (although one can get around this restriction if map-reduce calls are used and the query is restricted in the same way as shown for find)
  • all functions which include DB calls need an implicit context parameter
  • the json object in the DB must contain access information (e.g. team on a post to filter for teams)

by Tom Bocklisch


Related posts