How Access Control Works in Dgraph

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.

The requirements

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:

  • We should be able to create user accounts, e.g. Alice and Bob, and groups, e.g. devs and SREs.
  • Each user can be assigned to multiple groups.
  • Access control should be enforced on predicates, and each predicate should have 3 types of access:
    1. Read: being able to query data under the predicate
    2. Write: being able to mutate data under the predicate
    3. Modify: being able to alter the schema of the predicate.
  • Access control should be granted on the granularity of groups. For example, we can grant Read access of the predicate friend to the group dev, and Read+Write access of the predicate friend to the group sre.
  • There should be a root user account that can do any type of access to any predicate in the db. The root account is needed to bootstrap access control by creating other users’ accounts and setting up the initial ACL rules. Also the root account is necessary for mediation if ACL rules are misconfigured.
  • Multiple predicates matching a regular expression, e.g. ^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.
  • There should be a way to query:
    1. All the groups of a given user
    2. All the users belonging to a given group
    3. All the predicates and the granted permissions associated with a given group.
  • There should be logging of all the access attempts, including who is trying to perform what operation on which predicate, and whether the access is accepted or denied.

How ACL data is stored

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: Predicates assigned to groups

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 group
  • dgraph.password is for storing a user’s hashed password
  • dgraph.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).

I AM GROOT

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.

ACL clients

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 for changing and querying ACL data
  • The clients used to access data protected by ACL rules, e.g. the dgo client written in Go, the dgraph4j client in Java, the Ratel web portal etc.

The command line tools

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 group
  • dgraph acl del is for deleting a user account or a group
  • dgraph acl mod is for changing a user’s password, or a group’s permissions on predicates
  • dgraph acl info is for querying a user’s groups, or a group’s users, or predicate permissions granted to a group

Concrete examples for turning on the ACL feature in a cluster and setting up ACL rules can be found at our documentation page.

Client Libraries

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
    }
  }
}

The JWTs

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.

Access Token

Access Token

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.

Refresh Token

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.

Refresh Token

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.

Logging

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: ACL Log in Jaeger

Show case

Below is a quick demo of the ACL feature in action using the command line tool. ACL demo

Got feedback?

That covers all the aspects of our first ACL release. We cannot wait to hear your feedback at our discussion forum.


  1. 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. ↩︎