From the 102 session in the Couchbase LIVE New York mobile track, we continued iterating on the Couchbase Mobile sample application that was described in code from the “Couchbase Mobile 101: How to Build Your First Mobile App” session.
In the Couchbase Mobile 102 session, we explored Sync Gateway in depth on its features and on “How to Add Secure Sync to your Mobile Applications” by securing the Grocery Sync sample application which can be found on the Github repo for iOS and Android. In this blog, we will walk through what was discussed on Sync Gateway, which is the component that ties together Couchbase Lite and Couchbase Server; you may reference the slides on the topic and code snippets that are outlined below.
Key Mobile Data Security Concerns
One area that Sync Gateway helps resolve is data replication and this is regarding on how data is synchronized back and forth from the cloud to your mobile application on device. The authentication of your users before replication can occur is another key area that Sync Gateway addresses upon which the data partitioning happens. Once the replication is in place, data would need to be partitioned accordingly for your users to determine where specific data will be distributed. And then there is data access control where given a user that has been authenticated, Sync Gateway may help with setting up the read and write permissions accordingly. Let us explore each of these in more detail.
[1] User Authentication
With the pluggable authentication model that Couchbase Mobile supports, there are a variety of ways to implement authentication where Sync Gateway allows for custom configurations to the security framework of the application architecture. Sync Gateway supports three public providers with ‘Basic Auth’, ‘Facebook’, and ‘Persona’ authentication models.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "facebook" : { "register" : false }, "databases": { "grocery-sync": { “server”:”http://cbserver:8091”, “bucket":"grocery-sync", "users": {"GUEST": {"disabled": true}}, "sync":`function(doc) {channel(doc.channels);}` } } } |
A lot of the security questions are in the form of how we manage read and write access. Sync Gateway allows for defining fine-grain security policies for read side policies at the document level and then define the write side policies down to the field level. The general policy enforcement framework is based on a Javascript Sync function where it is flexible to define complex security rules to extend out the mobile application running Couchbase Lite.
Sync Function: Sync Gateway Configuration
The Sync Function is core to Sync Gateway on how to manage read-write access and is where the majority of the data access rules get defined. So it is really the core of your security implementation where the Sync Function is a JavaScript function that gets executed whenever any JSON document gets written to Sync Gateway.
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "databases": { "grocery-sync": { “server”:"http://walrus:", “bucket":"grocery-sync", "users": {"GUEST": {"disabled": true}}, “sync”:`function(doc,oldDoc) { channel(doc.channels); }` } } } |
The function is defined in the Sync Gateway configuration file where the method signatures are taking the current revision of the document, doc, and the previous revision, oldDoc. The body of the method is where the security rules are defined based on those two inputs. This is where a simple basic Sync Function may first be defined and then slowly more advanced security rules may build out to cover all the cases. With this approach, we can modify the sync function as new document types are defined or if there are an changes to the JSON schema.
Sync Function: Write Permission
- requireUser(): Takes as input a list of user id and validates whether the currently active users are part of that list
- requireRole(): Takes as input list of roles, where we can see if the requireUser has been granted that particular role. If they have not, then the document will get rejected.
- requireAccess(): Takes as input a list of channels where it is taking a list of current users and their list of channels to see if they have been granted a particular channel. If the user is not part of the list then they will get rejected.
- throw(): The throw lets you do whatever inspection you want on the incoming document. You can have validation there and say for example if a document is type ‘item’ and if the type does not match then you can reject the document. Similarly you can have validation that is based on a value being within a certain range. Therefore the very granular field level type validation that can be implemented in the Sync Function, may be used with a throw() method on the documents that do not meet those criteria. With throw() you can also provide error details on why it is thrown and the reason documents are rejected.
Sync Function: Read Permission
A particular channel will become into existance as soon as the channel command is used to assign an user to a channel. At that point, the user will be looking for documents that is tagged with the same channel name. There exist two special channels with the star [*] and exclamation [!] parameters.
- Star[*]: The * Star channel is the channel where every document is automatically added to that channel. An example is where if a guest user is granted the * star channel, they will have open ended access to all the documents. A clear use-case for this channel attribute would be similar to admin privileges where any user that that has the * star in their channel will be able to see everything in the system.
- Exclamation[!]: The second special channel is the ! exclamation attribute that is used to describe the public channel that is focused from the document’s perspective where if you add a document to the public channel, then every user will be able to see that particular document. A clear use-case for this channel attribute would be similar to having a document for public announcements or broadcasting a message to all users.
Sync Function: By Example
In the following example, we will explore using the features described above on how to secure the Grocery Sync Application from the Couchbase Mobile 101 session where it exists as a completely unsecured application with no privacy. Iterating with Sync Gateway, users will end up seeing only their own items as oppose to seeing the same list where all the available items that are added.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
{ "log" : [“*”], "databases": { "grocery-sync": { “server”:”walrus:”, “bucket":"grocery-sync", "users": { "GUEST": { "disabled": false, “admin_channels” : [“*”] } } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ ”log" : [“*”], "databases": { "grocery-sync": { “server”:”walrus:”, “bucket":"grocery-sync", "users": { “alice”: { “disabled” : false, “password”: “password”, “admin_channels”:[“*”] }, “bob”: { “disabled” : false, “password”: “password”, “admin_channels”:[“*”] } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{ ”log" : [“*”], "databases": { "grocery-sync": { “server”:”walrus:”, “bucket":"grocery-sync", "users": { “alice”: { “disabled” : false, “password”: “password”, “admin_channels”:[“*”] }, “bob”: { “disabled” : false, “password”: “password”, “admin_channels”:[“*”] } }, “sync” : ‘ function(doc, oldDoc) { //Add placeholder sync function, add custom read/write logic here }‘ } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
{ ”log" : [“*”], "databases": { "grocery-sync": { “server”:”walrus:”, “bucket":"grocery-sync", "users": { “alice”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-alice”] }, “bob”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-bob”] } }, “sync” : ‘ function(doc, oldDoc) { //Add placeholder sync function, add custom read/write logic here } ‘ } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
{ ”log" : [“*”], "databases": { "grocery-sync": { “server”:”walrus:”, “bucket":"grocery-sync", "users": { “alice”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-alice”] }, “bob”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-bob”] } }, “sync” : ‘ function(doc, oldDoc) { channel(“items-”+doc.owner); } //Add item document to owner’s items channel ‘ } } } |
When a document is being written into Sync Gateway, we assign it to a channel where the channel name is of the form of “items”-prefixed with the value of the owner property from within the owner document that is obtain by “doc.owner.” The previous configuration is when the document property is set to ‘bob’ and the ‘items’ would then be assigned to the ‘items-bob’ channel. The user, “bob” has access to the “items-bob” channel and basically that is the channel that Bob has been given access to in his user record. Now any item that has owner ‘bob’ as a property will be seen through that channel by Bob himself but it will not be seen by Alice cause it will not be in the ‘items-alice’ channel. What occurred in the configuration is that we assigned a document to an appropriate channel and also programmatically gave access to the owner through their owner “items-” channel.
So now users can only see/read their own items and effectively their own grocery lists. But they can still write to each other’s list cause Bob could upload an item and set the owner property to be “Alice” where in effect, Bob may easily infratrate Alice’s Grocery list. The next step in adding security is to make sure that when a document comes in, we would have the owner-property match with the current authenticated user.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
{ ”log" : [“*”], "databases": { "grocery-sync": { “server”:”walrus:”, “bucket":"grocery-sync", "users": { “alice”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-alice”] }, “bob”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-bob”] } }, “sync” : ‘ function(doc, oldDoc) { requireUser(doc.owner); //The owner of the item document must be the authenticated user channel(“items-”+doc.owner); } ‘ } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
{ ”log" : [“*”], "databases": { "grocery-sync": { “server”:”walrus:”, “bucket":"grocery-sync", "users": { “alice”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-alice”] }, “bob”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-bob”] } }, “sync” : ‘ function(doc, oldDoc) { if (doc.type == “friends”) { //process new friends document requireUser(doc.owner); //The owner of the friends access(doc.friends, “items-”+doc.owner); channel(“private-”+doc.owner); access(doc.owner, “private-”+doc.owner) } else { requireUser(doc.owner) channel(“items-”+doc.owner); } } ‘ } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
{ ”log" : [“*”], "databases": { "grocery-sync": { “server”:”walrus:”, “bucket":"grocery-sync", "users": { “alice”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-alice”] }, “bob”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-bob”] } }, “sync” : ‘ function(doc, oldDoc) { if (doc.type == “friends”) { //process new friends document requireUser(doc.owner); //The owner of the friends access(doc.friends, “items-”+doc.owner); channel(“private-”+doc.owner); access(doc.owner, “private-”+doc.owner); } else if (doc.type == “item”) { requireUser(doc.owner); channel(“items-”+doc.owner); } else{ throw({forbidden: “Invalid document type”}); } } }‘ } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
{ ”log" : [“*”], "databases": { "grocery-sync": { “server”:”walrus:”, “bucket":"grocery-sync", "users": { “alice”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-alice”] }, “bob”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-bob”] } }, “sync” : ‘ function(doc, oldDoc) { if (doc.type == “friends”) { //process new friends document requireUser(doc.owner); //The owner of the friends access(doc.friends, “items-”+doc.owner); channel(“private-”+doc.owner); access(doc.owner, “private-”+doc.owner); } else if (doc.type == “item”) { requireAccess(“items-”+doc.owner) channel(“items-”+doc.owner); } else{ throw({forbidden: “Invalid document type”}); } } ‘ } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
{ ”log" : [“*”], "databases": { "grocery-sync": { “server”:”walrus:”, “bucket":"grocery-sync", "users": { “alice”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-alice”] }, “bob”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-bob”] } }, “sync” : ‘ function(doc, oldDoc) { if (doc.type == “friends”) { //process new friends document requireUser(doc.owner); //The owner of the friends access(doc.friends, “items-”+doc.owner); channel(“private-”+doc.owner); access(doc.owner, “private-”+doc.owner); } else if (doc.type == “item”) { requireAccess(“items-”+doc.owner) if (oldDoc == null) { if (doc.check == true) { throw( {forbidden: “new items cannot be checked”}); } } channel(“items-”+doc.owner); } else { throw( {forbidden: “Invalid document type”}); } } ‘ } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
{ ”log" : [“*”], "databases": { "grocery-sync": { “server”:”walrus:”, “bucket":"grocery-sync", "users": { “alice”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-alice”] }, “bob”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-bob”] } }, “sync” : ‘ function(doc, oldDoc) { if (doc.type == “friends”) { //process new friends document requireUser(doc.owner); //The owner of the friends access(doc.friends, “items-”+doc.owner); channel(“private-”+doc.owner); access(doc.owner, “private-”+doc.owner); } else if (doc.type == “item”) { requireAccess(“items-”+doc.owner) if (oldDoc == null) { if (doc.check == true) { throw( {forbidden: “new items cannot be checked”}); } else { if (doc.check != oldDoc.check) { requireUser(doc.owner); } } } channel(“items-”+doc.owner); } else { throw( {forbidden: “Invalid document type”}); } } ‘ } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
{ ”log" : [“*”], "databases": { "grocery-sync": { “server”:”walrus:”, “bucket":"grocery-sync", "users": { “alice”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-alice”] }, “bob”: { “disabled” : false, “password”: “password”, “admin_channels”:[“items-bob”] } }, “sync” : ‘ function(doc, oldDoc) { if (doc.type == “friends”) { //process new friends document requireUser(doc.owner); //The owner of the friends access(doc.friends, “items-”+doc.owner); channel(“private-”+doc.owner); access(doc.owner, “private-”+doc.owner); } else if (doc.type == “item”) { requireAccess(“items-”+doc.owner) if (oldDoc == null) { if (doc.check == true) { throw( {forbidden: “new items cannot be checked”}); } else { if (doc.owner != oldDoc.owner) { throw({forbidden: “Quits Stealing Items”}); } if (doc.check != oldDoc.check) { requireUser(doc.owner); } } } channel(“items-”+doc.owner); } else { throw( {forbidden: “Invalid document type”}); } } ‘ } } |
[3] Data Transport on the Wire
Now, aside from the authentication and data read/write for user access concerns with security, the next topic is to ensure your data is secured going on the wire to a remote endpoint. Sync Gateway supports SSL and TLS on the transport. It is straight forward to configure within the Sync Gateway config file to enable SSL on Sync Gateway for your mobile app to have additional data security.
[4] Data Storage
For data storage on the client, you are really doing file system encryption on the device. There is good information on what you need to do to encrypt the local Couchbase Lite database on the mobile developer portal. We are talking about a secure cloud environment and configuring that for file system encryption on the client.
Summary
Sync Gateway is really the piece that glues together Couchbase Lite, the framework that lives on the embedded device. You have your Couchbase Server running in the cloud and Sync Gateway ties those together. Sync Gateway has a handful of key functions that it provides to turn an application that is running on your local device, which is a standalone disconnected application into a fully feature multi user synchronizing experience. What is shown above is how we can serialize things in the database by iterating on the Sync Function configuration file.
Next we will go into the Couchbase Lite HTTP listener component class is the Couchbase Mobile 103 session on how to enable the Peer-to-Peer feature where you can create unique social in-app experiences by “Building a Peer-to-Peer App with Couchbase Mobile.”