ACLs and Advanced Permission Model

Last updated 3 months ago

ACLs (Access Control Lists) allow you to fine tune the permission model to suit the use cases of your applications. ACLs are also resolved and applied on message delivery so ACLs can also be used to manage message routing. The core concepts of the ACL subsystem are:

  1. Privilege : A privilege denotes the ability of an actor to perform a certain operation. An example would be the read privilege on messages that allow anyone holding that privilege to read the message.

  2. Accessor : Any actor to which privileges can be granted. Within the mitter.io context, all accessors are users.

  3. Accessor selector : A selector denotes a class of accessor. An example of a selector is participant(channel-id:status) which denotes the set of all users that are participants in channel-id holding status as their Participation status.

  4. ACL : An ACL is a list of privilege-selector pairs.

  5. ACL Entity : Any entity to which ACLs can be applied. Message, Channel, User, TimelineEvent are all ACL entities.

Example

To illustrate how ACLs function within mitter.io, let us take an example of a Message with id msg sent to a channel with id chnl by the user axe. A Message is an ACL entity to which the following privileges can be applied:

  1. ReadMessagePrivilege() read_message

  2. DeleteMessagePrivilege() delete_message

From the section Basic permissions, we know that:

  1. Any participant of a channel can read a message from the channel.

  2. Any authenticated user can delete a message that they sent to a channel.

  3. Any authenticated user can send a message that they sent to a channel.

Both [2] and [3] apply even if the user is no longer a participant on the channel. To implement this, the given message has the following ACLs applied to it:

Message(id=msg)
acls = [+read:participant(chnl), +delete:user(axe), +read:user(axe)]

When we send a message these ACLs are applied by default and this is how we implement the basic permission model that is present even for applications that don't use ACLs.

If axe now wanted to send a message to the channel that only rylai could read, he would send the message with his custom ACLs:

Message(id=msg)
acls = [+read:user(rylai), +read:user(axe), +delete:user(axe)]

Whenever you supply a list of ACLs, the default ACLs are no longer applied. So any behavior you wish to be retained must be reflected by the ACLs you set. When this message is sent, mitter.io will even compute the delivery targets based on these ACLs and the message will be sent only to rylais device and only she will see it when she fetches messages via HTTP.

Now take the example where axe wants to send a message to everyone in the channel except rylai because rylai has been acting way too cool recently. To do so, he can set the ACLs as:

Message(id=msg)
acls = [-read:user(rylai), +read:participant(chnl), +read:user(axe), +delete(axe)]

When this message is received, mitter.io will not send messages to any of rylais delivery endpoints nor will she be able to fetch it via HTTP calls.

ACL Computation

An ACL is applied over two lists, the p-list an the m-list. The p-list consists of all ACLs that have a + permission and similarly the m-list consists of all ACLs that have a - permission. To check whether a given user has access to a privilege on an entity, we check if:

  1. At least one of the accessors that resolve to the user is in the p-list.

  2. None of the accessors that resolve to the user in the m-list.

Both these conditions need to be true for the ACL to pass as positive and the privilege to be considered granted. This model has certain limitations, but in the current phase of development, this is the model that allows us to maintain performance while still providing feature rich ACLs.

The limitations manifest in certain ways, for instance:

  1. +read:user(axe), -read:participant(chnl) would still result in the read privilege not being granted to axe

  2. A empty p-list results in no privileges being granted to any user. This however is not permitted in the system due to default ACLs and sticky ACLs kicking in, which is covered further below.

Sticky ACLs and Default ACLs

As we saw in the previous section, .system has complete access of all the data of the application. Moreoever, certain privileges, like -revoke_tokens_for_self is a privilege that a user is always granted and this cannot be overriden. To keep these constants in place, there are two ACL lists that are maintainted for each entity:

  1. Default ACLs : These are the ACLs that are applied to the entity if no ACLs were found on the entity.

  2. Sticky ACLs : These ACLs are always implicitly present on the entity and cannot be overriden.

For instance, one of the sticky ACLs on channel is +add_participant:user(.system) which means that you can always add a participant to any channel using an application key/secret.

Available Accessor Selectors

The following selectors are available:

  1. user(user-id) Represents a single user having the provided id.

  2. participant(channel-id:status) Represents the group of users which are a participant in the channel with the given channel id, with a particular participation status.

  3. any_user() Represents any authenticated user.

Do note that the ACLs user(.system) and user(.anonymous) are forbidden.

COMING SOON Soon, users can be assigned an aclTag and all users belonging to an acl tag can be selected using acltag(tag-name).

ACL Entitys and Privilege List

In the following section we will be listing all the ACL entities and the permissions that are available on them. Do note that mitter.io currently only supports setting custom ACLs on messages with support for Channel and User coming up in the next release. We will also be releasing partial access to ACLs on Application via certain configuration options in the subscriber panel.

Channel

A channel can have the following privileges on it:

  1. join_channel Join the channel (add oneself)

  2. add_participant_to_channel Add any other user as a participant to the channel

  3. list_participants List the participants of the channel

  4. remove_participant Remove any other user as a participant to the channel

  5. remove_self Remove oneself from the channel

  6. delete_messages_from_channel Delete messages from the channel

  7. read_from_channel Read the channel object and send messages to it

  8. send_to_channel Send messages to the channel with themselves as the sender

  9. send_as_other_to_channel Send messages to the channel with anyone else as the sender

For instance, if you wanted to introduce a role called admin for the channel, who alone can add participants to the channel, but anyone could remove themsleves, you would assign the following privileges to channel:

Channel(chnl) acls = [
+add_participant_to_channel:user(admin), +remove_participant:user(admin),
-join_channel:any_user(), +remove_self:any_user(),
+read_from_channel:participant(chnl), +send_to_channel:participant(chnl)
]

NOTE The actual accessor selector for a participation selector is participant(chnl:status), but for the sake of brevity we have omitted it. Similarly, the actual privileges on messages are read_message and delete_message.

The default ACLs for a channel are:

  1. +read_from_channel:participant(channel-id:Active)

  2. +send_to_channel:participant(channel-id:Active)

  3. +list_participants:participant(channel-id:Active)

  4. +join_channel:any_user()

  5. +remove_self:any_user()

The sticky ACLs for a channel are:

  1. +read_from_channel:user(.system)

  2. +send_as_other_to_channel:user(.system)

  3. +remove_participant:user(.system)

  4. +add_participant:user(.system)

  5. +list_participants:user(.system)

  6. -join_channel:user(.system)

Message

A Message can have the following privileges assinged to it:

  1. read_message Read the message

  2. delete_message Delete the message

These ACLs can be used to selectively send messages to a message from a user that only certain people in the channel can receive. Do note that if a user is assigned an ACL for read for a message sent to a channel, but the user is not a participant in the channel, the user will not be able to read the message unless the user also has the read_from_channel privilege on the channel the message was sent to.

The default ACLs for a message sent to a channel with id channel-id by sender-id are:

  1. read_message:participant(channel-id:Active)

  2. read_message:user(sender-id)

  3. delete_message:user(sender-id)

The sticky ACLs for a message sent to a channel with id channel-id by sender-id are:

  1. read_message:user(.system)

  2. delete_message:user(.system)

Application

Application ACLs are different only in the sense that they define certain privileges for users at a global level. They contain privileges on the creation of users and channels and control default aspects of the application in general.

NOTE Certain privileges on the application will soon be made available on the user object itself, as the user will be defined as an ACL entity. At that point in time, these privileges will no longer be available on an application.

Access to these ACLs will be made available, but via a controlled manner via the application panel. There is no plan to ever support modifying the direct ACL list of the application.

The following privileges can be assigned to an application:

  1. create_channel - Create a channel

  2. create_message - Create a message

  3. create_user - Create a user

  4. list_channels - List all the channels in the application

  5. list_user_data - List the user data for a user (other than oneself)

  6. write_user_credentials - Revoke/Issue tokens for a user (other than oneself)

The second permissions is a little special as it only applies for OutOfBand messages and is currently not applied or in use anywhere. The privilege list_channels is required to list the channels in an application, but only those channels will be returned for which the user has a read_from_channel privilege. The subtle difference that manifests is that if the user does not have that privilege on any channel, this call will return an empty list, but if the user does not have list_channels privilege, it will return a 403 Forbidden (with a missing_privileges error code). A user can not have the list_channels privilege but still send and receive messages to a channel for which they have the appropriate send_to and read_from privileges.

The default ACLs for an application are:

  1. create_channel:any_user()

  2. list_participants:any_user()

The sticky ACLs for an application are:

  1. create_channel:user(.system)

  2. create_message:user(.system)

  3. create_user:user(.system)

  4. list_channels:user(.system)

  5. list_user_data:user(.system)

  6. write_user_credentials:user(.system)

Do note that despite not having explicit ACLs, the following behavior is ALWAYS true and cannot be overridden in any way:

  1. A user can always revoke their own tokens.

  2. A user can always request for additional tokens.

  3. .system cannot list user data for itself or issue user tokens for itself.

Fine-grained Control Over ACLs

Introduced in v0.4, you can now not only specify ACLs for an entity up-front, but also modify later according to your specific use-cases.

To modify an ACL, there are two modes that are supported:

  1. PatchType.Set

  2. PatchType.Diff

A PatchType.Set operation overwrites all ACLs for a given entity as is specified in the payload. A PatchType.Diff operation on the other hand specifies the specific ACLs that are to be added/removed for an entity.

Let's take an example of a channel that we just created:

POST /v1/channels
{
"channelId": "my-channel",
... common fields ..,
"appliedAcls": [
"-join:any_user()"
]
}

What this does is, it creates the channels, adds any participants that were specified in the request, but will not allow any new user to join as -join:any_user() revokes the join privilege from every user. Do note that a user can still add some other participant to the channel, since the add_participant privilege is still available to them. Ideally, we should have revoked both the permissions, but for the sake of brevity we will continue with this example with the single privilege.

Now, after some time, we want to allow users to join the channel. There are two ways of doing this:

  1. Either we remove the -join:any_user() rule from the channel.

  2. We reset the ACLs to the default state i.e. empty ACLs where the default ACLs for channel will kick-in.

To do this using the first strategy, we will use a Diff type of ACL modification:

PATCH /v1/channels/my-channel/acls
{
"patchType": "Diff",
"addAcls": [],
"removeAcls": [
"-join:any_user()"
]
}

This will return a response which will contain two channel objects, one before the ACL modification and one after it:

{
"oldEntity": { "channelId": .... },
"newEntity": { "channelId": .... }
}

We can also use a Set type operation and instead do:

PATCH /v1/channels/my-channel/acls
{
"patchType": "Set",
"setAcls": {}
}

A Set ACL operation performs a complete set of all ACL records for a given entity. So if we specify an empty object, it will set the entities ACLs to an empty ACL list, which means that for any operations henceforth, it will result in the default ACLs for that entity to kick in.

ACLs for entities can be patched except for Applications. For data-integrity purposes, we currently do not allow any ACLs on Application to be modified (or specified up-front for that matter).