Valve Introduction
Learn permissions with Valve
deepstream uses a powerful permission-language called Valve that allows you to specify which user can perform which action with which data.
With Valve you can
- Restrict access for individual users or groups
- Permission individual actions (e.g. write, publish or listen)
- Permission individual records, events or rpcs
- validate incoming data
- compare against stored data
Requirements
For this tutorial it’s helpful to know your way around the deepstream configuration as we’ll need to tell the server where we stored our permissioning rules. deepstream supports a variety of communication concepts such as data-sync, publish-subscribe or request-response and Valve is flexible enough to allow different rules for each concept. This guide will mostly focus on records, so it’d be good to familiarize yourself with them. Since permissioning is fundamentally about the rights of individual clients, it would also be good to know how user authentication works in deepstream.
Let’s start with an example
Imagine you are running a discussion forum. To avoid vandalism and spam, users
have to wait 24 hours before they can create new posts or modify existing posts
after registration.
This means we’ll need to store the time the user registered along with their account information. This can be done dynamically using http authentication, but to keep things simple for this tutorial we’ll just store it as timestamp
within the serverData
using deepstream’s file-based authentication. A user entry in conf/users.yml
might look as follows:
JohnDoe:
password: gvb4563Z
serverData:
timestamp: 1482256123052
The snippet above shows a user JohnDoe
. The server hosting the forum needs to
know when John Doe registered so there is a timestamp
in the serverData
section.
With deepstream as a back-end, it makes sense to store all forum threads in records (this is the data-sync concept). The following Valve snippet gives new users read-only access:
record:
"*":
read: true
listen: true
delete: false
create: "user.data.timestamp + 24 * 3600 * 1000 < now"
write: "user.data.timestamp + 24 * 3600 * 1000 < now"
The record
label signifies that the following rules apply to operations
involving records; the pattern in the line below is a wild card matching every
record name. In deepstream, records can be created, written to, deleted, read from, and you can listen to clients subscribing to a record. With Valve, you can have different permissions for each of these actions. In the Valve snippet
above, we permit everyone to read records, listen to subscription, and we
disallow record deletion. Finally, in the last two lines we grant users create
and write
permissions only if the accounts are older than 24 hours by
comparing the timestamp
from the user’s serverData
with the current time; now
returns Unix time like Date.now()
in JavaScript, in milliseconds and 24 * 3600 * 1000 milliseconds are 24 hours.
Lastly, we need to update the config file to make use of our custom
permissions. Assuming we stored the permissions in the path
conf/permissions.yml
, we can instruct the deepstream server to load our
settings with the following lines in conf/config.yml
:
permission:
type: config
options:
path: ./permissions.yml
As you saw above, setting up deepstream’s file-based permissioning facilities requires a file with permissioning rules, changes to the configuration file, and optionally some user-specific data.
Permissioning
A generic Valve rule might look as follows:
concept:
"pattern":
action: "expression"
For every action, there is usually a corresponding function in the client API,
e.g., the record write
permissions are needed when calling record.set()
in
the JavaScript client API. Every record, RPC, event, and authenticated user in
deepstream possesses a unique identifier (a name) and if Valve wants to find out
if a certain operation is permitted, then
- it looks for the appropriate section in the permissioning file for records, RPCs, or events, and so on,
- it searches for the rule with the best match between pattern and identifier, and
- it evaluates the right-hand side expression. In the following paragraphs, we present the possible actions.
File Format
The Valve language uses YAML or
JSON file format and the file with the
permissioning rules must always contain rules for every possible identifier
because the server will not supply default values. Note that the deepstream
server ships with a permissions file in conf/permissions.yml
which permits
every action. Valve is designed to first and foremost use identifiers to match
permissionable objects with corresponding rules. Thus, identifiers should be
chosen such that rules can be selected only based on the identifier.
Identifier Matching
Valve can match identifiers using fixed (sub-)strings, wild cards, and
placeholders (with deepstream, we call them path variables); these
placeholders can be used in the expressions. Suppose we store a user’s first
name, middle name, and last name in the format
name/lastname/middlename/firstname
and have a look at the following Valve
code:
presence:
'name/Doe/$middlename/$firstname':
allow: false
User names that match this rule are, e.g., John Adam Doe (in this case, the
record identifier is name/Doe/Adam/John
) or Jane Eve Doe
(name/Doe/Eve/Jane
); in the former case, $firstname === 'John'
and in the
latter case $firstname === 'Jane'
.
The wild card symbol in Valve is the asterisk (the symbol *
) and *
matches
every character until the end of the string. Placeholders start with a dollar
sign followed by alphanumeric characters and match everything until a slash is
encountered. In principle, identifiers can contain any character. Nevertheless,
if you use an asterisk in an identifier, deepstream offers no way to match
specifically this character.
Expressions
After identifier matching, deepstream will evaluate the right-hand side expression. The expression can use a subset of JavaScript including
- arithmetic expressions,
- the conditional operator,
- comparison operators,
- the string functions
startsWith
,endsWith
,indexOf
,match
,toUpperCase
,toLowerCase
, andtrim
.
Additionally, you can use the current time (on the server) with now
, you can
access deepstream data, and cross-reference records.
Any deepstream client needs to log onto the server and the user data can be
accessed with Valve but note that user’s are not necessarily authenticated
unless this is forbidden in the config. You can check for authenticated users
with user.isAuthenticated
(the ternary operator ?:
may prove useful when checking
this property). If a client authenticated, its user name can be accessed with
user.name
and its server data with user.data
. Additionally, Valve allows
you to examine data associated with a rule, e.g., for a record, this means one
can examine old and new value. Since the data is dependent on the type (record,
event, or RPC, and so on), we will discuss this detail in the sections on the
specific types.
Valve gives you the ability to cross reference data in your records. In your
right-hand side expression, use the term _(identifier)
to access the record
with the given identifier, where identifier
is interpreted as a JavaScript
expression returning a string, e.g., _('family/' + $lastname)
. The cross
referenced record must exist. Note that cross references ignore Valve
permissions meaning you gain indirect read access irrespective of the Valve
rules.
When evaluating expressions, you need to keep several pitfalls in mind. Using
the current time with now
requires you to consider the usual limitations of
time-dependent
operations
on computers and additionally, now
is evaluated on the server; you should keep
this in mind whenever a client uses the current time in its code. Valve allows
you to cross reference stored data but this is computationally expensive. Thus,
the default config shipped with deepstream allows no more than three cross
references as of December 21, 2016. Finally, the usual warnings about type
coercion (implicit type conversions), JavaScript comparison
operators,
and floating-point arithmetic
apply.
Records
Records can be created, deleted, read from, written to, and you can listen to other clients subscribing to records (the record tutorial elaborates on these operations and it explains the differences between unsubscribing from, discarding, and deleting records). The following snippet is the default Valve code for records:
record:
"*":
create: true # client.record.getRecord()
read: true # client.record.getRecord(), record.get()
write: true # record.set()
listen: true # record.listen()
delete: false # record.delete()
In Valve, you can access the current record contents by referencing oldData
and for the write
operation, the modified record can be examined with data
.
Note that create
permissions are only invoked by getRecord()
if the
requested record does not exist, otherwise only reading rights are required.
Similarly, writes are always successful if the record does not have to be
modified, e.g., modified and unmodified record are identical. Moreover, if a
write operation is rejected by the server, then the client must handle the
resulting error message; otherwise the client copy of the record will be out of
sync with the server state. Finally, do not mix up the path
given to
record.get()
and record.set()
with the record identifier that is used by
Valve.
User Presence
deepstream can notify you when authenticated users log in. The permissioning key
is called presence
and the only option is to allow or disallow listening:
presence:
"*":
allow: true # client.subscribe()
Events
Events can be published and subscribed to.
Moreover, a client emitting events may listen to event subscriptions. The
actions can be permissioned in the section events
:
events:
"*":
publish: true # client.event.emit()
subscribe: true # client.event.subscribe()
listen: true # client.event.listen()
The publish
action allows the examination of the data by referencing data
in
the expression.
RPCs
Remote procedure calls can be provided
or requested. The corresponding permissioning section is identified by the key
rpc
:
rpc:
"*":
provide: true # client.rpc.provide()
request: true # client.rpc.make()
Configuring for File-Based Permissioning
To use file-based permissioning, the config file must contain the key
permission.type
with the value config
. The name of the permissioning file
must be provided in the deepstream config file under the key
permission.options.path
and can be chosen arbitrarily. If a relative path is
used to indicate its location, then this path uses the directory containing the
config file as base directory.
In summary, if the permissioning rules can be found in conf/permissions.yml
and if the configuration file is conf/config.yml
, then a minimal config for
file-based permissioning looks as follows:
permission:
type: config
options:
path: ./permissions.yml
Further Reading
More compact introductions (or refreshers) are the tutorials Valve Permissioning Simple, Valve Permissioning Advanced, and Dynamic Permissions using Valve. To learn how to sent user-specific data using Valve, have a look at the user-specific data guide.