MongoDB Realm Sync Permissions Explained
I grew up in Paris France and went to French high-school there. There is a lot I loved about the culture, but one of the most frustrating aspects of French pedagogy was their inability to synthesize a complex subject down to something that was understandable. The term they used for anyone who succeeded in this task was called an oeuvre de vulgarisation — which literally translates to a masterpiece for the vulgar. In a my own attempt to explain MongoDB Sync Realm Permissions, I now present my own chef-d’œuvre de vulgarisation. I suspect that if you are reading this article, you probably have more than a vulgar interest in MongoDB Realm and SwiftUI collaborative programming.
In June 2020, MongoDB released a beta version of a new product called MongoDB Realm — a synchronized real-time database for mobile and web applications. This new product was the result of merging the Realm Cloud database with the MongoDB Atlas database. The Realm Cloud product was a client/cloud synchronizing database for developing collaborative applications on top of iOS and Android mobile devices. The Realm Cloud product has subsequently been discontinued.
MongoDB Realm is a backend as a service, whose closest competitor is perhaps Google Firebase. It allows mobile and web developers to easily build synchronizing applications on top of a scalable cloud database without having to understand the implementation details of the supporting server architecture. From a productivity point of view, it is a major time saver. As far as MongoDB is concerned, their business model is to license instances of MongoDB Atlas — the supporting backend database behind MongoDB Realm. I was a Firebase developer for two years before embracing Realm Cloud — now all my focus is on MongoDB Realm. The biggest advantage of MongoDB Realm is its offline-first approach to synchronization.
MongoDB Altas is an NOSQL database that is organized around objects in collections that conform to a specific schema. Realm adds another mapping on top of this structure using the concept of a partition key. In Realm, the developer must first define an application and its partition key. Typically, the partition key is called “_partition”, but can be named anything the developer choses. The only requirement is that once defined, it cannot be changed. The partition key must be defined as a property on a collection if the objects that belong to it are to be synchronized by the application. In most cases, the partition key type is a string. The value of a partition key specifies which Realm the synchronized object belongs to. From the application’s point of view, a Realm is a database container of sorts. In the old Realm Cloud product, a Realm was the top level database within a Realm Instance that supported the application. In MongoDB Realm, a Realm is a database subset whose objects and collections all share the same partition key value. The partition key value provides an isomorphic mapping between the two concepts. To access the objects with a Realm, the application must first open the Realm with a specific partition key value, as shown in the Swift code below.
The MongoDB Realm documentation explains well enough how to set up partitions for an application. Where it is lacking is with best practices for a partition strategy, specifically a range of concrete examples where partitions can be used to solve specific synchronization/collaboration problems. With the end goal of discovering these best practices, I gave myself the task of writing a simple multi-user chat program that segmented chat threads into separate partitions. I called this application: SimpleRealmChat. At Cosync, we are developing a number MongoDB Realm extensions to bridge the gap between the MongoDB Realm database and the needs of developers who use it. Over the next few months, we are going to release a chat module that can plugin to any MongoDB Realm application, and provide multi-user chat functionality with minimal effort on behalf of the developer.
The SimpleRealmChat application is ultra simple in its design. It allows a user to signup and/or login through a simple email/password authentication, and then presents a searchable list of all the users in the system. When the user of the iOS device clicks on a specific user, he/she is then presented a private chat thread with the selected user. The application is written in Swift, using SwiftUI, for iOS only. We are working on a React/Native version due out later this month.
When thinking about a implementing a simple chat program, what is the best way to design the Realm partition strategy? Clearly, each user would need their own private partition to write out data that is pertinent to the user. In our case, this private information is simply the user’s first and last name. As a convention, the partition key value for the user’s partition should be the user’s Realm user id. This partition should only be readable and writable by the user and no one else — this where UserPrivateData should be stored. Second, all users will need to read profile data for all the other users in the system. This partition called the shared partition; it should be readable by all users and only writable by the system user in backend functions. The shared partition is where we store the UserProfile objects. Lastly, each chat thread between two users should be stored in a separate partition that is only readable and writable by the users in the chat. If the system were to be hacked on the client side, it should not be possible for any other users to access the chat thread. By convention, the chat partition value for a chat thread should be a concatenation of the two user ids that can access thread
<userId 1>_<userId 2>
Where <userId 1> is lexicographically less than <userId 2>.
In summary there are three partition types used by our SimpleRealmChat application:
- User partitions (value is “<userId>”)
- Shared partition (value is “shared”)
- Chat partitions (value is “<userId 1>_<userId 2>”)
Sync access rules for the user and shared partitions are easy enough to express using Realm’s JSON expression language. Expressing access rules for the chat partitions will involve enabling Custom User Data for the application.
The MongoDB Realm documentation provides a good explanation on how to Enable Custom User Data — a database collection used to store private information about each user. The SimpleRealmChat application will use the custom data collection to store a list of chat partition values for those partitions that a user can access. The key thing to remember when defining Custom User Data is to first create the database and the collection in Atlas before enabling Custom User Data for the Realm application. In our SimpleRealmChat application, we created a database called CustomUserDB and a collection called CustomUserData to support Custom User Data.
The last thing to remember is that in the Rules section: do not configure the CustomUserData collection by choosing a Permissions Template. If no rule exists for the namespace CustomUserDB.CustomUserData, the collection will not be writable from within the client application. This is what you want, so that no client can spoof the Custom Data and potentially violate security. This way, the Custom Data is readable by the client but no more than that. The backend server function updateChatPartitions will take care of writing to the Custom Data collection.
Realm provides a simple user interface within the Realm Application Portal to edit the Sync Rules for an application. In order to actually edit the Permissions, the developer must first terminate the Sync. Also, rules cannot be applied if the Development Mode is on, otherwise the default values of read and write being true will be selected. Sync access rules are only intended to be applied when the application goes into production mode and is released.
The Sync Rules language provided by MongoDB Realm is somewhat cryptic and relies heavily on macro expression variables. The language reminds me of days gone by programming XSLT stylesheets for XML data, or of my very early experience with JCL punch cards for the IBM 360 mainframe; writing PL/1 batch programs. A simple misspelled variable will lead to nothing working, with very little indication as to why. Another issue I discovered with sync rules is that what appears on the left and right side of the rule can affect whether it works or not, i.e. “%%partition” : “shared” is not the same thing as “shared”: “%%partition”. This can be extremely confusing to say the least. In other words, the left and right sides of a sync rule are not transposable.
The Read Sync rules for the SimpleRealmChat application should look like this:
The Write Sync rules should look like this:
The shared partition should only be readable by a user, not writable. A chat partition should only be readable/writable by a user if it is listed in the chatPartitions property of the custom user data collection. A user’s partition (which is the Realm user id) should be readable and writable by the user.
The “%%user.custom_data.chatPartitions”: “%%partition” statement will cause the Realm system to look up the user’s custom user data object in the CustomUserData collection (in the CustomUserDB database) and check the chatPartitions property, which contains an array of partition key values for the chat partitions that the user can access. The contents of the custom user data can be examined in Atlas or in Compass as shown below:
From a security perspective, it is important to remember that a user cannot access a chat partition, unless the chat partition value is listed in the chatPartitions property of their custom user data object. And since CustomUserDB is only readable by a client side application — writable by the server function code — it cannot be “spoofed” by a malicious client side application. Data is only protected if it is segregated, and this is something that MongoDB Realm does exceedingly well with its partition architecture and server side functions. Incidentally, this is something that Google’s Firebase product does not handle very well.
This sample code will require a MongoDB account, where the developer will have to setup authentication and database triggers. All of the steps are outlined in the attached README.md file. The developer will also need the latest copy of Xcode to build the sample chat application for iOS.
The purpose of this exercise was to show how easy it is to use MongoDB Realm partitions to enforce strict read/write privileges on the shared data used by a collaborative program. This is both important from a security perspective as well as a scalability perspective. By isolating chat data into distinct partitions (or Realms), a multi-user chat program is far more scalable — and greatly limits the synchronization overhead required by the server. Clients will only synchronize chat data that they are directly participating in, and not all the chat data that occurs systemwide.
To all my fellow developers out there, enjoy your Realming.