Archive for March 28, 2019

How to create Dynamic Custom Roles with MongoDB Stitch

None of us want to write lots of code to control what data each user can access. In most cases, you can set up your data access controls in seconds by using Stitch’s built-in templates, e.g., “Users can read and write their own data” or “Users can read and write their own data. Users that belong to a sharing list can read that data”. Stitch also lets you create custom roles that you tailor to your application and schema – this post creates such a rule, querying a second collection when deciding whether to allow a user to insert a document.

The application is a troll-free timeline where you can only tag another user in a post if that user has labeled you as a trusted friend. I’ve already created a database called safeplace for the collections.

The users collection includes a list of usernames for their trusted friends (peopleWhoCanTagMe):

{
    "_id" : ObjectId("5c9273127558d01d93f53dc0"),
    "username" : "alovelace",
    "name" : {
        "first" : "Ada",
        "last" : "Lovelace"
    },
    "peopleWhoCanTagMe" : [
        "jeckert",
        "jmauchly"
    ]
}

You then need to create a data access rule for the posts collection, consisting of 2 roles, which Stitch evaluates in sequence:

Creating custom roles for Stitch data access control rules

The second, anyReader, role lets anyone read any post, but the role we care about is allowedTagger which controls what the application can write to the collection.

The allowedTagger role is defined using this JSON expression:

{
  "%%true": {
    "%function": {
      "name": "canTheyTag",
      "arguments": [
        "%%root.poster",
        "%%root.tagged"
      ]
    }
  }
}

%%root represents the document that the user is attempting to insert. The poster and tagged attributes of the document to be written are the usernames of the author and their claimed friend. The JSON expression passes them as parameters to a Stitch function named canTheyTag:

exports = function(poster, tagged){
  var collection = context.services.get("mongodb-atlas")
    .db("safeplace").collection("users");
  return collection.findOne({username: tagged})
  .then ( userDoc => { 
    return (userDoc.peopleWhoCanTagMe.indexOf(poster) > -1);
  }).catch( e => { console.log(e); return false; }); 
};

This searches the users collection for the tagged user and then checks that the poster’s username appears in the peopleWhoCanTagMe array in the retrieved document.

You can test this new rule using the mongo shell (courtesy of Stitch’s MongoDB Connection String feature). Initially, I’m not included in Ada’s list of friends and so trying to tag her in a post fails:

db.posts.insert({ 
    poster: "amorgan", 
    tagged: "alovelace", 
    post: "Just sent you a pull request" })

WriteCommandError({ "ok" : 0, "errmsg" : "insert not permitted" })

The logs show that the insert didn’t match our customer role but that it did match the second (anyReader), but that inserts aren’t allowed for that role:

Logs:
[
  "uncaught promise rejection: StitchError: insert not permitted"
]
Error:
role anyReader does not have insert permission
Stack Trace:
StitchError: insert not permitted
{
  "name": "insertOne",
  "service": "mongodb-atlas"
}
Compute Used: 936719 bytes•ms

Ada then adds me as a trusted friend:

db.users.update(
    {username: "alovelace"}, 
    {$push: {peopleWhoCanTagMe: "amorgan"}}
)

My second attempt to tag her in a post succeeds:

db.posts.insert({
    poster: "amorgan", 
    tagged: "alovelace", 
    post: "Just sent you a pull request" })

WriteResult({ "nInserted" : 1 })

I recently had someone ask me how to implement “traditional database roles” using Stitch, i.e. an administrator explicitly defines which user ids belong to a specific role, and then use that role to determine whether they can access a collection. You can use this same approach for that use case – have a collection that assigns users to roles and then find the user id in that collection from a Stitch function that’s used in roles for the collections you want to protect. You could then optimize the rules by including the users’ roles as an attribute in their authentication token – but I’ll save that for a future post!