A newer version of this documentation is available.

View Latest

Sync Function

      +

      Defining sync functions for effective data routing and access control in the cloud-to-edge synchronization of enterprise data.
      The sync function is crucial to the security of an app. It is in charge of data validation, access control and routing.

      Related access-control topics: Sync function | Read access | Write access

      Sync Function Basics

      The Sync Function is a JavaScript function whose source code is stored in the Sync Gateway’s database configuration file. You can learn more about this property ($db.sync) in the Configuration Schema Reference — see: sync.

      The sync function is called every time a new revision/update is made to a document, and the changes to channels and access made by the sync function are tied to that revision. If the document is later updated, the sync function will be called again on the new revision, and the new channel assignments and user/channel access replace the ones from the first call.

      It can do the following things:

      For simple applications it might be the only server-side code you need to write. For more complex applications it is still a primary touchpoint for managing data routing and access control.

      Configuration

      Add the sync function defined into your sync-gateway-config.json — see Example 1

      Example 1. Example Sync Function
      //
      {
        //  ... may be preceded by additional configuration data as required by the user ...
        "databases": {
          "getting-started-db": {
            "server": "http://localhost:8091",
            "bucket": "getting-started-bucket",
            "username": "sync_gateway", (1)
            "password": "password", (2)
            "enable_shared_bucket_access": true, (3)
            "import_docs": true,
            "num_index_replicas": 0, (4)
            "users": {
              "GUEST": { "disabled": false, "admin_channels": ["*"] },
            },
            "sync": `function (doc, oldDoc) { (5)
              if (doc.sdk) {
                channel(doc.sdk);
              }
              }`,
              //  ... may be followed by additional configuration data as required by the user ...
          }
        }
      }
      //

      Configuration properties:

      1 This is the username of the user you created on the Couchbase Server Admin Console.
      2 The password of the user that you created on the Couchbase Server Admin Console.
      3 The Sync with Couchbase Server feature allows Couchbase Server SDKs to also perform operations on this bucket.
      4 num_index_replicas is the number of index replicas stored in Couchbase Server, introduced with GSI/N1QL indexing — see Indexing versus Views. If you’re running a single Couchbase Server node for development purposes the num_index_replicas must be set to 0.
      5 The sync function — a javascript function enclosed in backticks, which is actioned every time a new document, document revision or deletion is made to a database — see the: databases.$db.sync property.

      Function Definition

      The sync function is crucial to the security of your application. It is in charge of data validation, and of authorizing both read and write access to documents.

      The sync function should be a focus of any security review of your application.

      The API is high-level, enabling you to do some powerful things very simply. However, you do need to remain vigilant. Review the function carefully to ensure it detects threats and prevents any illegal access.

      You write your sync function in JavaScript and provide it in the sync property of the appropriate database in your sync-gateway-config.json file.

      The basic structure of the sync function, within the configuration file, looks like this:

      sync: `
      function (doc, oldDoc) {
          // Your code here
      }
      `
      Remember to include your JavaScript code within a pair of back ticks (`).

      The sync function arguments are:

      doc

      An object, the content of the document that is being saved. This matches the JSON that was saved by the Couchbase Lite and replicated to Sync Gateway. The _id property contains the document ID, and the _rev property the new revision ID. If the document is being deleted, there will be a _deleted property with the value true.

      oldDoc

      If the document has been saved before, the revision that is being replaced is available in this argument. Otherwise it’s null. (In the case of a document with conflicts, the current provisional winning revision is passed in oldDoc.) Your implementation of the sync function can omit the oldDoc parameter if you do not need it (JavaScript ignores extra parameters passed to a function).

      If you don’t supply a sync function, Sync Gateway uses the default Sync Function.

      channel(channelname)

      The Sync Function provides the channel() function to route the document to the named channel(s). It accepts one or more arguments, each of which must be a channel name string, or an array of strings. The channel function can be called zero or more times from the sync function, for any document.

      Here is an example that routes all "published" documents to the "public" channel:

      function (doc, oldDoc) {
         if (doc.published) {
            channel("public");
         }
      }

      Routing changes has no effect until the document is actually saved in the database, so if the sync function first calls channel() or access(), but then rejects the update, the channel and access changes will not occur.

      As a convenience, it is legal to call channel with a null or undefined argument; it simply does nothing. This allows you to do something like channel(doc.channels) without having to first check whether doc.channels exists.
      Channels don’t have to be predefined. A channel implicitly comes into existence when a document is routed to it.

      access(username, channelname)

      Documents can grant users access to channels. The access() function grants access to a channel to a specified user. It can be called multiple times from a sync function.

      The first argument can be an array of strings, in which case each user in the array is given access. The second argument can also be an array of strings, in which case the user(s) are given access to each channel in the array. As a convenience, either argument may be null or undefined, in which case nothing happens.

      If a user name begins with the prefix role:, the rest of the name is interpreted as a role rather than a user. The call then grants access to the specified channels for all users with that role.

      You can use the all channels wildcard ('*') to grant the user access to all documents in all channels.

      The effects of all access calls by all active documents are effectively unioned together, so if any document grants a user access to a channel, that user has access to the channel. Calling access(username, channelname) multiple times to grant the same user access to the same channel will result in negative performance implications.

      The following code snippets shows some valid ways to call access():

      access ("jchris", "mtv");
      access ("jchris", ["mtv", "mtv2", "vh1"]);
      access (["snej", "jchris", "role:admin"], "vh1");
      access (["snej", "jchris"], ["mtv", "mtv2", "vh1"]);
      access (null, "hbo");  // no-op
      access ("snej", null);  // no-op

      role(username, rolename)

      The role() function grants a user a role, indirectly giving them access to all channels granted to that role. It can also affect the user’s ability to revise documents, if the access function requires role membership to validate certain types of changes. Its use is similar to access — the value of either parameter can be a string, an array of strings, or null. If the value is null, the call is a no-op.

      For consistency with the access call, role names must always be prefixed with role:. An exception is thrown if a role name doesn’t match this. Some examples:

      role ("jchris", "role:admin");
      role ("jchris", ["role:portlandians", "role:portlandians-owners"]);
      role (["snej", "jchris", "traun"], "role:mobile");
      role ("ed", null);  // no-op
      Roles, like users, have to be explicitly created by an administrator.

      Unlike channels, which come into existence simply by being named, you can’t create new roles with a role() call. Like users, they must be defined in the configuration or by API.

      Nonexistent roles don’t cause an error, but have no effect on the user’s access privileges.

      You can create roles retrospectively. As soon as a role is created, any pre-existing references to it take effect.

      requireUser(username)

      The requireUser() function authorizes a document update by rejecting it unless it’s made by a specific user or users, as shown in the following example:

      // Throw an error if username is not "snej":
      requireUser("snej");
      
      // Throw an error if username is not in the list:
      requireUser(["snej", "jchris", "tleyden"]);

      The function signals rejection by throwing an exception, so the rest of the sync function will not be run. All properties of the doc parameter should be considered untrusted, since this is after all the object that you’re validating. This may sound obvious, but it can be easy to make mistakes, like calling requireUser(doc.owners) instead of requireUser(oldDoc.owners). When using one document property to validate another, look up that property in oldDoc, not doc!

      requireRole(rolename)

      The requireRole() function authorizes a document update by rejecting it unless the user making it has a specific role or roles, as shown in the following example:

      // Throw an error unless the user has the "admin" role:
      requireRole("admin");
      
      // Throw an error unless the user has one or more of those roles:
      requireRole(["admin", "old-timer"]);

      The argument may be a single role name, or an array of role names. In the latter case, the user making the change must have one or more of the given roles.

      The function signals rejection by throwing an exception, so the rest of the sync function will not be run.

      requireAccess(channels)

      The requireAccess() function authorizes a document update by rejecting it unless the user making it has access to at least one of the given channels, as shown in the following example:

      // Throw an exception unless the user has access to read the "events" channel:
      requireAccess("events");
      
      // Throw an exception unless the user can read one of the channels in the
      // previous revision's "channels" property:
      if (oldDoc) {
          requireAccess(oldDoc.channels);
      }

      The function signals rejection by throwing an exception, so the rest of the sync function will not be run.

      You can specify multiple channel names, for example:
      requireAccess('any channel name', '*')'

      If a user was granted access using only the all channels wildcard] (*), then requireAccess('any channel name')' will fail because the user wasn’t granted access to that channel (only to the * channel).

      requireAccess() will only recognize grants made explicitly using a channel name (not by wildcard).

      requireAdmin()

      The requireAdmin() function authorizes a document update by rejecting it unless it is being sent to the Sync Gateway Admin REST API.

      // Throw an exception unless the request is sent to the Admin REST API
      requireAdmin();

      throw()

      At the most basic level, the sync function can prevent a document from persisting or syncing to any other users by calling throw() with an error object containing a forbidden: property. You enforce the validity of document structure by checking the necessary constraints and throwing an exception if they’re not met.

      Here is an example sync function that disallows all writes to the database it is in.

      function(doc) {
         throw({forbidden: "read only!"})
      }

      The document update will be rejected with an HTTP 403 "Forbidden" error code, with the value of the forbidden: property being the HTTP status message. This is the preferred way to reject an update.

      In validating a document, you’ll often need to compare the new revision to the old one, to check for illegal changes in state. For example, some properties may be immutable after the document is created, or may be changeable only by certain users, or may only be allowed to change in certain ways. That’s why the current document contents are given to the sync function, as the oldDoc parameter.

      We recommend that you not create invalid documents in the first place. As much as possible, your app logic and validation function should prevent invalid documents from being created locally. The server-side sync function validation should be seen as a fail-safe and a guard against malicious access.

      expiry(value)

      Calling expiry(value) from within the sync function will set the expiry value (TTL) on the document.

      expiry("2018-07-06T17:00:00+01:00")

      Under the hood, the expiration time is set and managed on the Couchbase Server document (TTL is not supported for databases in walrus mode). The value can be specified in two ways:

      • ISO-8601 format: for example the 6th of July 2016 at 17:00 in the BST timezone would be 2016-07-06T17:00:00+01:00;

      • as a numeric Couchbase Server expiry value: Couchbase Server expiries are specified as Unix time, and if the desired TTL is below 30 days then it can also represent an interval in seconds from the current time (for example, a value of 5 will remove the document 5 seconds after it is written to Couchbase Server). The document expiration time is returned in the response of GET /\{tkn-db}/\{doc} when show_exp=true is included in the querystring.

      The behavior on the resulting document when the expiry value is reached depends on the following:

      if Mobile-Web Data Sync is enabled

      The active revision of the document is tombstoned. If there is another non-tombstoned revision for this document (i.e a conflict) it will become the active revision. The tombstoned revision will be purged when the server’s metadata purge interval is reached.

      if Mobile-Web Data Sync is disabled

      The document will be purged from the database.

      As with the existing explicit purge mechanism, this applies only to the local database; it has nothing to do with replication. This expiration time is not propagated when the document is replicated. The purge of the document does not cause it to be deleted on any other database.

      Document Conflicts

      If a document is in conflict there will be multiple current revisions. The default, "winning" one is the one whose channel assignments and access grants take effect.

      Handling deletions

      Validation checks often need to treat deletions specially, because a deletion is just a revision with a "_deleted": true property and usually nothing else. Many types of validations won’t work on a deletion because of the missing properties — for example, a check for a required property, or a check that a property value doesn’t change. You’ll need to skip such checks if doc._deleted is true.

      Example

      Here’s an example of a complete, useful sync function that properly validates and authorizes both new and updated documents. The requirements are:

      • Only users with the role editor may create or delete documents.

      • Every document has an immutable creator property containing the name of the user who created it.

      • Only users named in the document’s (required, non-empty) writers property may make changes to a document, including deleting it.

      • Every document must also have a title and a channels property.

        function (doc, oldDoc) {
          if (doc._deleted) {
            // Only editors with write access can delete documents:
            requireRole("role:editor");
            requireUser(oldDoc.writers);
            // Skip other validation because a deletion has no other properties:
            return;
          }
          // Required properties:
          if (!doc.title || !doc.creator || !doc.channels || !doc.writers) {
            throw({forbidden: "Missing required properties"});
          } else if (doc.writers.length == 0) {
            throw({forbidden: "No writers"});
          }
          if (oldDoc == null) {
            // Only editors can create documents:
            requireRole("role:editor");
            // The 'creator' property must match the user creating the document:
            requireUser(doc.creator)
          } else {
            // Only users in the existing doc's writers list can change a document:
            requireUser(oldDoc.writers);
            // The "creator" property is immutable:
            if (doc.creator != oldDoc.creator) {
                    throw({forbidden: "Can't change creator"});
            }
          }
          // Finally, assign the document to the channels in the list:
          channel(doc.channels);
        }