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:
- Read: being able to query data under the predicate
- Write: being able to mutate data under the predicate
- 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 groupdev
, and Read+Write access of the predicatefriend
to the groupsre
. - 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:
- All the groups of a given user
- All the users belonging to a given group
- 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: ![](upload://gcpXJ26pG6bP41fZRmmUbUzgCBP.png)
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 1. 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)
.
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 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 LibrariesThe 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 TokenHere 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.
Refresh TokenTherefore, 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: ![](upload://4LcQGzjKMryw2U97dVGyVveorA4.png)
Show case
Below is a quick demo of the ACL feature in action using the command line tool. ![](upload://nwQseXdmOr2jl4ybq8yA7cP4u4h.gif)
Got feedback?
That covers all the aspects of our first ACL release. We cannot wait to hear your feedback at our discussion forum.
Top image: On December 5th, 2001, STARSHINE-2 was launched from the Space Shuttle Endeavour on STS-108This is a companion discussion topic for the original entry at https://blog.dgraph.io/post/access-control-in-dgraph/