With GDPR passing in the EU on April 14th, 2016, users on the Internet are demanding protection of their privacy and data from Internet companies. The regulation has profound implications. Many companies take the law and users’ demand seriously: they start encrypting data for traffic within their data centers, and employing strict control on who can access users’ data. The Dgraph team takes full notice of that. Access control on the data stored in Dgraph is very important to our enterprise customers. This feature has been in-demand and we plan to release it in the upcoming Dgraph v1.1 as a proprietary feature. In this article, you will find some details about how Access Control List (ACL) works in Dgraph.
We started the project by asking what are the features that we must support for access control to work both securely and seamlessly for our customers. After a few rounds of brainstorming and feedback, we settled on the following list:
friend
to the group dev
, and Read+Write access of the predicate friend
to
the group sre
.^user(.\*)name$
, can be granted to a group at once. When
such a rule exists, predicates created later matching the regex should have access control
enforced automatically.With the requirements settled, now let’s dive into some details. The very first detailed design question is how to store the ACL data. We discussed several options each having their pros and cons. In the end we decided to pick the following approach:
To make it easier for operators to turn on the ACL feature, we are storing the ACL rules inside Dgraph instead of relying upon an external system. The example demonstrates a cluster with a Zero group and a number of Alpha groups. The Zero group maintains metadata in the cluster, including which group a server belongs to and which group a predicate is assigned to. The Alpha groups host the actual user data, with each one hosting a portion of the whole data set. The whole dataset is divided into shards and in Dgraph sharding is based on predicates.
To store the ACL data, we need a total of four predicates. And these four predicates are treated specially by group zero in the sense that they are always assigned to group 1. Other groups maintain a local in-memory cache of all the ACL rules and refresh it periodically by querying group 11. Thus, every Alpha server knows about the ACL rules, so each Alpha is capable to applying those rules to all the incoming requests, without having to make network calls.
Now let’s take a closer look at the four predicates:
dgraph.xid
is for storing the name of a user or groupdgraph.password
is for storing a user’s hashed passworddgraph.user.group
is for storing user → group mapping and uses a reverse index to retrieve group → users mapping.dgraph.group.acl
is for storing all predicates assigned to a group (or regexps against which predicates will be matched), and the types of access allowed for
the group, e.g. (friend, Read), (name, Read+Write), (^user.*name$, Read+Write+Modify)
.These predicates are proposed by an Alpha server when it starts up if the ACL feature is turned on through command line options and created when they do not already exist in the cluster. Unlike a regular data predicate, they are not removed by the DropAll request.
Also being created during an Alpha server start-up time is the root user account if it does not
already exist, and we give it the nickname groot
(graph root) along with a default password which
can be changed using our command line tools explained below.
Now that the server side storage is ready, let’s switch our attention to the client side. From the ACL perspective, there are two types of clients:
The command line tools are available under a new acl
subcommand within the
Dgraph binary.
dgraph acl add
is for creating a new user account or a new groupdgraph acl del
is for deleting a user account or a groupdgraph acl mod
is for changing a user’s password, or a group’s permissions on predicatesdgraph acl info
is for querying a user’s groups, or a group’s users, or predicate permissions granted
to a groupConcrete examples for turning on the ACL feature in a cluster and setting up ACL rules can be found at our documentation page.
The second type of clients, e.g. dgo or dgraph4j, accesses data in Dgraph by
executing transactions. Each transaction can have a mix of query
and mutate
requests. Also a client can be used to alter
the schema, e.g. changing a
predicate type from an int to a string. When a request is received by an Alpha
server, it is evaluated using rules in the ACL cache and is only accepted if the
operations for all predicates in the request are allowed. For example, for the
query below to work, the current user must belong to one or more groups that
collectively have been granted Read access to both the name
and friend
predicate.
{
friends_of_alice(func: eq(name, "Alice")) {
name
friend {
name
}
}
}
But wait a second! How does an Alpha server know the query’s current user or its groups? The trick is that each request carries some metadata encoded as JSON Web Token that tells the Alpha server about the current user and the groups they belong to.
Here is how JWT works: Before running any transactions, the client, e.g. dgo
needs to make a Login
call to an Alpha server using a user name and password.
When processing the Login
request, the Alpha server 1) queries all of the
user’s groups, 2) puts an expiration timestamp depending on a server-configured
TTL, and 3) signs all that data with a secret key. The group list, expiration
timestamp, and signature together form an access JWT which is sent back to the
client. The client then remembers this JWT and attaches it to every subsequent
request it sends to the cluster, e.g. the query request example we saw earlier.
As explained above, JWTs contain the groups a logged-in user belongs to. This can cause a problem if this user is assigned a new group. Until the Access Token expires, a new JWT would not be issued to reflect that change.
Therefore, Access Token expiration is typically set to a short duration, like a few hours. But, this causes an issue with long running services, which would suddenly get their access denied after this token expires.
When a Login
happens, a refresh token is issued as well. This token
is typically valid for a much longer duration, like a month. When access token
expires and an unauthorized response is received from the server, Dgraph clients
would automatically retry a login using the refresh token (if available). This would
cause a new JWT to be issued with the updated groups.
Notice that when an access JWT expiration causes an automatic login using the refresh JWT, not only does the access JWT get replaced, but the refresh JWT itself also gets renewed with a later expiration timestamp. Thus the same client can be used continually without an explicit call to Login again after it’s initialized, thanks to the rolling JWTs.
We log all of the failed access attempts in the main log files of Alpha servers, together with
other types of events. All the log entries for ACL have a prefix ACL-LOG
, so operators can run
a simple grep and filter them out. Here is an example log entry:
I0320 18:36:44.386417 1 access_ee.go:650] ACL-LOG Authorizing user "alice" with groups "" on predicates "name,friend" for "Read", allowed:false
To reduce verbosity of logging, we send the authorized access attempts as OpenCensus traces, which can be further analyzed with Jaeger. Below is a screenshot of the ACL log from the Jaeger UI:
Below is a quick demo of the ACL feature in action using the command line tool.
That covers all the aspects of our first ACL release. We cannot wait to hear your feedback at our discussion forum.
For now, each refresh retrieves the full set of ACL data. In the future, we’ll switch to using a live stream of updates which is in the works. ↩︎