• log out

Access Control

The Streams plugin implements another very useful piece of core functionality: access to data. Like all other features in Qbix, it is implemented in a standard way that integrates with other parts of the system, including Users and Contacts. This is another reason why you should use streams for (nearly) all your persistence needs.

Read level

There are three types of access to streams, each with its own access level. The read level controls the user's ability to see the contents of a stream. It is a numeric value ranging from 0 to 40 -- the greater the level, the more the user can see. At the moment, the following read levels are defined: 'none', 'see', 'content', 'participants', 'messages'. You can test for these using $stream->testReadLevel($level) in PHP or stream.testReadLevel(level) in JS in Node or on the client.

Write level

The write level controls the user's ability to change the contents a stream, post messages to it, etc. It is a numeric value ranging from 0 to 40 -- the greater the level, the more the user can change. At the moment, the following read levels are defined: 'none', 'join', 'vote', 'suggest', 'postPending', 'post', 'relate', 'relations', 'suggest', 'edit', 'closePending', 'close'. You can test for these using $stream->testWriteLevel($level) in PHP or stream.testWriteLevel(level) in JS in Node or on the client.

Admin level

The admin level controls the user's ability to affect others' access in the stream. It is a numeric value ranging from 0 to 40 -- the greater the level, the more the user can do. At the moment, the following read levels are defined: 'none', 'tell', 'invite', 'manage', 'own'. You can test for these using $stream->testAdminLevel($level) in PHP or stream.testAdminLevel(level) in JS in Node or on the client.

Permissions

Streams can also have custom strings representing permissions to do various things in the stream, such as registerForMe and highlight. You can test permissions with $stream->testPermission($permission) in PHP and stream.testPermission(permission) in JS.

Interface

Most tools provided by the Streams plugin and others that require it, intelligently modify the interface they present to the user to only expose actions they can take, based on their access to the stream. Thus, for example, a Streams/inplace tool will present a way to edit the stream only if the user has permissions.

In your client Javascript, you can bring up a standard dialog to allow the user to modify their access to the stream. The standard appearance and functionality of this dialog allows the user to feel comfortable with it across different apps built on the platform:

Q.Streams.Dialogs.access(publisherId, streamName, callback);

Publishers

The user publishing the stream has the highest access levels to it (i.e. 'max'). This design decision expresses the idea that, in a distributed social network, the publisher may decide to host their own website on their own servers, under their control. Thus, the system realizes that, in principle, the publisher can do anything they want to the data they publish, and expresses this in the permissions system.

Public access

The Streams_Stream row itself contains readLevel, writeLevel and adminLevel. The values stored here are for the public's access to the stream, i.e. the default "user on the street" as well as those who aren't logged in.

Granular access

The Qbix system implements role-based access control where each publisher can use their contact labels for assigning access. A person user can have labels like: friends, family, etc.. An organization user can have labels like: managers, teachers, etc.

A stream can have one or more corresponding Streams_Access rows in the database, which store the non-public granular access. Each row would have either ofUserId or ofContactLabel filled. The user fetching the stream may belong to the stream publisher's contacts under more than one label. The {read|write|access}Levels are computed as the maximum across all contact labels, so for example, if someone is both a "School/teacher" and an "School/admin", their readLevel would be whichever is bigger. The permissions are likewise aggregated.

On the other hand, Streams_Access rows with userId are used to set access for specific people. They override the label-based access, and are used for e.g. banning someone or granting extra privileges to them. This is true for access levels as well as permissions. (To override only some access levels for a specific user, put -1 for the access levels to leave alone).

Inviting users

In the article about invites, you'll learn more about how you can grant access to certain users by sending them an invite. Invites are a bit like capabilities, in that whoever gets the "secret" invitation link will be able to sign in as the invited user and get those permissions. That's why invitations are delivered directly by Qbix to specific endpoints, whether they are an email address, mobile number, facebook app-to-user notification, etc.

A user cannot invite someone to have greater access than they, themselves, have. When it comes to readLevel and writeLevel, the user can confer the same access level or lower. When it comes to accessLevel, an invite can only confer a smaller level than the inviting user. For example, if the inviting user has an "manage" adminLevel, they can invite others and give them an "invite" adminLevel, allowing them to invite others but not be admins. When a user accepts an invite, the Qbix platform checks the access of the inviting user to the stream one more time, and confers at most the permissions they currently have. This is so that a user cannot send an invite to someone (including themselves) to get around getting banned or downgraded permissions. For example, if someone's ability to post messages to a stream is taken away, they can't simply go and accept an invite they sent to themselves to get it back.

Fetching streams

When fetching streams in PHP, you should specify which user you are fetching the stream as, so the access will be properly calculated for you to use the test{read|write|admin}Level() functions in PHP or JS.

// fetch multiple streams as a user
$rows = Streams::fetch($asUserId,
  $publisherId, $streamName, ...);
 
// fetch one stream as a user
$stream = Streams::fetchOne($asUserId,
  $publisherId, $streamName, ...);

To fetch as "the public", you should pass the empty string "" in place of the $asUserId. If you pass null in place of $asUserId, it will fetch as the logged-in user, or as "the public" is no one is logged in.

Creating new streams

You can create new streams using either the PHP, HTTP or JS API. When you are the publisher of the stream, you can create pretty much any stream. Sometimes, other publishers will allow you to create streams that they will publish on your behalf. This is determined by the Streams::isAuthorizedToCreate function, which is called internally.

For a publisher to allow another user to create a stream, there must be a stream template in the database, with this publisherId and with streamName the type of the stream with a slash at the end. If the logged-in user has adminLevel = "own" access to this template, they will be allowed to create streams of that type. The access, subscriptions, etc. of the template are copied to the newly created stream when it is created.

Another way the publisher may allow a user to create a stream is with templates and relations. If a user has at least has at least writeLevel = "relate" in a category stream (such as a photo album), and there are two templates:

  • Template A: of the type of the category stream
  • Template B: of the type of the stream the user wants to create

and if template B is related to template A using Streams_RelatedTo with a certain type, then this user will will be authorized to create a stream of type B, related to the category stream by a relation of that type.

As a simple example: photo galleries have type "Streams/images", while image streams have type "Streams/image". You can store two stream templates in the database named "Streams/images/" and "Streams/image/" under a given publisherId, and a relation with type "photos" between them. Then, the user will be able to successfully ask that this publisher create "Streams/image" streams related as "photos" to any stream of type "Streams/images" for which they have at least writeLevel = "relate".

The Streams HTTP API allows clients to create streams with HTTP POST requests. But, this will throw a Users_Exception_notAuthorized unless the config allows the client to create this particular type of stream, by having "Streams"/"types"/$streamType/"create" config field be either true or an array naming the fields that can be set by the client. Similar rules apply when the client tries to edit existing streams via HTTP PUT requests, except with "edit" instead of "create". Keep this in mind when designing your own stream types.

Mutable Access

Mutable access is similar to templates, except the name of the template is a stream type followed by an asterisk (*) instead of a slash (/). Unlike templates, users' access to streams isn't determined by copying access at the time the stream is created. Instead, it is computed at the time the stream is fetched. This mechanism is used to grant additional access at once to all streams of a certain type, even if they were already created before. When designing your schema, you should normally use templates for everyday operation, and mutable access for special user roles.

Closing streams

When a stream is closed, it is unrelated from all other streams, and marked for potential deletion. Write access for is restricted for all users who don't have writeLevel >= "close". Streams are not deleted right away, because the Streams/closed message needs to propagate to all clients that might be listening for real-time updates. The time of closure is recorded in the closedTime field and a little while later, a background service might remove this stream's information.

A client can send a PUT request to the Streams/stream action in order to reopen the stream, by setting closedTime=false, but only if the user doing this is the publisher of the stream or has accessLevel = "own".

Typical use cases

  • Google Docs: Each publisher can simply manage their contacts, access for each contact, invite people to manage certain streams, etc.
  • Facebook Groups: In Qbix this is called communities, each should have its own userId under which it publishes streams. Communities have contact labels which represent roles, and grant various access to various streams that they publish, so members can manage them together.

Unlike Google Docs and Facebook Groups, there is a lot more flexibility for determining who can do what in a given stream or community, using public access, templates, mutable access, or granular access.

Categories

Streams which serve as indexes, to which other streams are related, are referred to as "categories". Note that this designation depends on the direction of the relation. The Streams plugin defines a Streams/category stream type, whose icon by default looks like a folder. This can be used out of the box to allow users to browse and organize streams into a hierarchy, simply by placing a Streams/related tool on the page. Other stream types include Streams/file, Streams/image, Streams/audio, Websites/article, and more. You can also use other stream types as categories, such as Streams/gallery to hold images, or design your own stream types to do what you want.

Inheriting access

A stream can inherit access from one or more other streams. This slows down access calculations somewhat, and should be used sparingly. This is typically used when creating related streams so that users will have at least the same access to those related streams. Like with regular access, inherited access based on contact labels is aggregated, while inherited access set directly for users overrides contact labels (so, e.g. if a user is banned from a stream, they are also banned from any streams that inherit access from that stream).

The Streams_Stream row has an inheritAccess field where you can store JSON in the following format:

$stream->inheritAccess = Q::json_encode(array(
  array($publisherId1, $streamName1), ...
));

Conventions

When you have a document, game, chatroom, etc. it is represented by a stream. Users can be given various access to the stream, and they can potentially invite others, granting them access up to what they, themselves have. Invitations also serve as a quick way to onboard new users and put them straight in the middle of a group activity with other participants. Upon accepting an invitation, the user automatically joins the stream as a participant (so they get realtime updates pushed via socket.io to their web client when they are online), and they are also automatically subscribed to the stream (so they get offline notifications when none of their web clients are online). This combination of updates and notifications helps bring the user back and engage with the stream, until they unsubscribe from it or leave. Similarly, when a user publishes a new stream, they typically auto-join and auto-subscribe to it.

Remember that publishers have the maximum readLevel, writeLevel and adminLevel in a stream, and all custom permissions. This represents the concept that, in a distributed system, publishing information on your own server allows you to have full control over what happens there. The Qbix Platform, accordingly, is designed to discourage architectures where another user can restrict the stream's publisher from changing "their own" data.

Clients making requests to the server, however, can be restricted from creating and editing certain streams. This is accomplished by setting the "create" and "edit" config parameters for the stream type. By default they are false, not allowing clients to use the standard HTTP POST and PUT verbs to create and edit streams. But you can set them to true or even set them to be arrays of field names that can be modified by the client. Then the client can use the familiar Q.Streams Javascript API to create and edit streams of that type. Otherwise, your app or plugin might expose custom HTTP actions for the client, and a Javasript API for them, which would enable them to do custom actions in the streams. In that case, you are responsible for figuring out if the client is authorized to do those actions.

When you have a community (something like a facebook page), it is represented by a user. The community can install some apps and invite admins for those apps. The label for the admins in a given app is typically $app/admins. Note that contact labels are typically plural.

Admins can be given access to streams published by the community user, such as Streams/contacts and Streams/labels in order to manage contacts and labels of the community. By doing so, they can manage people's membership in the community, granting and revoking roles. (These streams also record the history of changes to the contacts and labels, and can push notifications of any new changes.)

A community may have many different roles for its members. Each app's installer typically defines a bunch of stream types (under its namespace) including default values, descriptions for messages, etc. It also installs templates in the database with typical defaults for access, subscriptions, etc.

When each user customizes the access or subscription on a particular stream, they may choose to update their personal templates for that type of stream. For example, if I set my subscription settings in a chess game to be notified only about messages of type "Chess/move", this will be the default whenever I subscribe to chess games in the future. Similarly, if I set a chess game to be viewable by friends, then the next time I start a chess game, this will be the default.

Each app has a main community, which is obtained with Users::communityId() or Q.Users.communityId() in Node.js . There is also communityName() and communitySuffix(). By default, they are derived from the app's name, but can (and should) be customized in the config under "Users"/"community".