Skip to content

Channels Access Control and Data Routing w Sync Function

Chris Anderson edited this page Jul 30, 2013 · 6 revisions

Channels: Access Control and Data Routing with the Sync Function

Example Code: There is also a tour of the CouchChat data model that is a good introduction to access control and data routing.

API Documentation: This page is an overview of the access control and data routing system. There are detailed API docs for the sync function here: Sync Function API

What are Channels?

Channels are the intermediaries between documents and users. Every document belongs to a set of channels, and every user has a set of channels s/he is allowed to access. Additionally, a replication from Sync Gateway specifies what channels it wants to replicate; documents not in any of these channels will be ignored (even if the user has access to them.)

Thus, channels have three purposes:

  1. Authorizing users to see documents;
  2. Partitioning the data set;
  3. Constraining the amount of data synced to (mobile) clients.

There is no need to register or preassign channels. Channels come into existence as documents are assigned to them. Channels with no documents assigned are considered empty.

Valid channel names consist of Unicode letter and digit characters, as well as "_", "-" and ".". The empty string is not allowed. The special meta-channel name "*" denotes all channels. Channel names are compared literally, so they are case- and diacritical-sensitive.

Mapping documents to channels

There are currently two ways to assign documents to channels. Both of these operate implicitly: there's not a separate action that assigns a doc to a channel, rather the contents of the document determine what channels its in.

Explicit property

The default (simple and limited) way is to add a channels property to a document. Its value is an array of strings. The strings are the names of channels that this document will be available through. A document with no channels property will not appear in any channels.

Sync function

The more flexible way is to define a sync function. This is a JavaScript function that takes a document body as input and can decide based on that what channels it should go into. It may not reference any external state and it must return the same results every time it's called on the same input.

The sync function is specified in the config file for your database. Each sync function applies to one database.

To add the current document to a channel, the function should call the special function channel which takes one or more channel names (or arrays of channel names) as arguments. For convenience, channel ignores null or undefined argument values.

Defining a sync function overrides the default channel mapping mechanism; that is, the document's channels property will be ignored. The default mechanism is equivalent to the following simple sync function:

function (doc) { channel(doc.channels); }

Replicating channels to Couchbase Lite

The basics are simple: When pulling from Sync Gateway, configure the replication to use a filter named sync_gateway/bychannel, and a filter parameter channels whose value is a comma-separated list of channels to fetch. The replication will now only pull documents tagged with those channels.

Removal from channels

There is a tricky edge case of a document being "removed" from a channel without being deleted, i.e. when a new revision is not added to one or more channels that the previous revision was in. Subscribers (downstream databases pulling from this db) should know about this change, but it's not exactly the same as a deletion.

Sync Gateway's _changes feed includes one more revision of a document after it stops matching a channel. It adds a removed property to the entry where this happens. (No client yet recognizes this property, though.) The value of removed is an array of strings, each string naming a channel this revision no longer appears in.

The effect on the client will be that after a replication it sees the next revision of the document, the one that causes it to no longer match the channel(s). It won't get any further revisions until the next one that makes the document match again.

This could seem weird ("why am I downloading documents I don't need?") but it ensures that any views running in the client will correctly no longer include the document, instead of including an obsolete revision. If the app code uses views to filter instead of just assuming that all docs in its local db must be relevant, it should be fine.

Note that in cases where the user's access to a channel is revoked, this will not remove documents from the user's device which are part of the revoked channels but have already been synced.

Authorization

NOTE distributed systems are tricky, and access control and authorization become interesting when clients are occasionally connected. Sync Gateway can only control the data it shares with client, not what clients do with it once they have it.

If you haven't read the CouchChat data model tour it's a good intro to this API.

The all_channels property of a user account determines what channels that user may access. Its value is derived from the union of:

  • The user's admin_channels property (which is settable via the admin REST API);
  • The channels that user has been given access to by access() calls from sync functions invoked for current revisions of documents (see "Programmatic Authorization" below);
  • The all_channels properties of all roles the user belongs to. (Which are themselves computed according to the above two rules.)

The only documents a user can access are ones whose current revisions are assigned to one or more channels the user has access to:

  • Any GET/PUT/DELETE request to a document not assigned to one or more of the user's available channels will fail with a 403.
  • _all_docs is filtered to return only documents that are visible to the user.
  • _changes ignores requests (via the channels parameter) for channels not visible to the user.

Write protection -- access control of document PUTs or DELETEs -- is done by document validation, this is handled in the sync function rather than a separate validation function. (See below.)

After a user is granted access to a new channel, the changes feed will incorporate all the existing documents in that channel, even those from earlier sequences than the client's since parameter. That way the next client pull will retrieve all those documents the user now has access to.

Programmatic Authorization

It is possible for documents to grant users access to channels; this is done by writing a sync function that recognizes such documents and calls a special access() function to grant access.

`access() takes two parameters: the first is a user name or array of user names; the second is a channel name or array of channel names. For convenience, null values are ignored (treated as empty arrays.)

A typical example is a document representing a shared resource (like a chat room or photo gallery), which has a property like members that lists the users who should have access to that resource. If the documents belonging to that resource are all tagged with a specific channel, then a sync function can be used to detect the membership property and assign access to the users listed in it:

function(doc) {
	if (doc.type == "chatroom") {
		access(doc.members, doc.channel_id)
	}
}

In this example, a chat room is represented by a document with a "type" property "chatroom". The "channel_id" property names the associated channel (with which the actual chat messages will be tagged), and the "members" property lists the users who have access.

access() can also operate on roles: if a username string begins with role: then the remainder of the string is interpreted as a role name. (There's no ambiguity here, since ":" is an illegal character in a user or role name.)

Since anonymous requests are authenticated as the user "GUEST", you can make a channel and its documents public by calling access with a username of GUEST.

Authorizing Document Updates

As mentioned earlier, sync functions can also authorize document updates. A sync function can reject the document by throwing an exception:

throw({forbidden: "error message"})

A 403 Forbidden status and the given error string will be returned to the client.

To validate a document you often need to know which user is changing it, and sometimes you need to compare the old and new revisions. For those reasons the sync function actually takes up to three parameters. To get access to the old revision and the user, declare it like this:

function(doc, oldDoc, user) { ... }

oldDoc is the old revision of the document (or empty if this is a new document.) user is an object with helper functions on it to validate updates.

user.requireUser("snej") // throw if username is not "snej"
user.requireUser(["snej", "jchris", "tleyden"]) // throw if username is not in the list
user.requireRole("admin") // throw an error unless the user has the "admin" role
user.requireRole(["admin", "old-timer"]) // throw an error unless the user has one of those roles
user.requireAccess("events") // throw an error unless the user has access to read the "events" channel
user.requireAccess(["events", "messages"]) // throw an error unless the can read one of these channels

NEXT: Authentication

Clone this wiki locally