Original post

The first in a 4-part series by community author Anthony Master.

Authentication, Authorization, Access Control… there is just so much going on that it can seem impossible to put it all together. So let’s do that right now… Before we get too far into this, let me give you a brief introduction of who I am, why the need for this tutorial, what we are going to build, and what you can expect to learn here.

Why the Need?

As we were early adopters of Dgraph, we were implementing this solution as fast as it could be developed. We have experienced being on the cutting edge of the cutting edge. Being here though sometimes we are figuring out how to use new features before documentation can even be written. I want to share with you what we learned to help propel your use of Dgraph.

What are we going to build?

I have seen the simple ToDo apps and a few other examples, but I wanted to take it up a step. We are going to build the foundation of an address book. Not just another address book, but a shared address book with granular group access control. And to top it all off, we are going to do this without the Enterprise license and can even host it on the free tier of Slash. I want to show you how to store user credentials, authenticate against the database, authorize access using controls we define, and enhance it with granular group control.

What to expect?

  • How to build a schema
  • A schema structure to support ACL
  • Granting granular control within groups
  • Defining rule sets for auth directive
  • Generating JWTs
  • Authenticating users against our dB without auth0
  • Writing custom mutations and bringing it into the graphql endpoint.

This is the first of a four part series:

  1. Building an Access Control Based Schema
  2. Authorizing Users with JWTs and Rules
  3. Authenticating Against a Dgraph Database and Generating JWTs
  4. Bringing Authentication into the GraphQL Endpoint as a Custom Mutation

PART 1 – Building an Access Control Based Schema

Where do we start? Before we authenticate or create rules to authorize users to do CRUD operations, we need to have a schema to base it all upon. In this schema, we will start with a place to hold the username and password. We will name this type “User”. To store the password we will put it in its own type to lock down access on it more.

NOTE: To follow along, you will need to be using Dgraph 20.07 or newer or be using a hosted backend on Slash GraphQL

type User {
  username: String! @id
  hasPassword: Pass!
}

type Pass {
  id: ID!
  password: String! @search(by:[hash])
}

Notice that we added @id directive on the username. This will make sure that usernames are unique as they act as a xid. We added the @search directive on password in the Pass type. This will allow us to filter based on matching passwords later on. The exclamation mark indicates a required field.

NOTE: We will be storing hashed passwords in a String scalar. There are other ways to do this, I am choosing this way to highlight auth rules. You could use the @secret directive if you desired.

The next type to add is “Contact”. This will store the contacts of the address book.

type Contact {
  id: ID!
  name: String!
  hasPhones: [Phone]
}

A Contact has phone numbers. Notice how we put the Phone type in brackets. This indicates that it can link to more than one. We used the type Phone, so let’s declare it now:

type Phone {
  id: ID!
  number: String!
  forContact: Contact @hasInverse(field: hasPhones)
}

The hasInverse directive will help keep this data connected. We could add more types like Addresses and Emails, but we will stop here for now. This is all pretty simple so far. We have Users that have Passwords, and we have Contacts that have Phone numbers. Next, we will start preparing for access control. The first part of this is expanding the User. Some users will be super admin, with higher privileges. We can keep track of this by adding a isType predicate to the User type with an enum field for the value

type User {
  #continuing from above
  isType: UserType! @search
}

enum UserType {
  USER
  ADMIN
}

Now that we can manage site administrators, let’s add a type to control individual access. Thinking this through, we want to grant some users as owners of Contacts with full access, some users as moderators with edit access and some as read only access. We want to add this access on Contact and their Phone numbers. This will allow a shared Contact to have a private Phone number. We will call our access type “ACL”. Before we write this next snippet, we want to limit access to User so users cannot see other usernames. But we still want to allow users to see who has access to their contacts. We can handle this by linking a Contact and a User together. We also want to allow public contacts in our address book shared to the world. Probably not the best idea for a real app, but this is for our learning.

type ACL {
  id: ID!
  level: AccessLevel!
  grants: Contact # We cannot force this as required because there will be instances where everyone can see public Contacts, but they cannot see who owns that Contact.
}

enum AccessLevel {
  VIEWER
  MODERATOR
  OWNER
}

type User {
  #continuing from above
  isContact: Contact # Not required as not every Contact will be a User
}

type Contact {
  #continuing from above
  isPublic: Boolean @search # We will assume this is false if not provided
  access: [ACL]
  isUser: User @hasInverse(field: isContact)
}

type Phone {
  #continuing from above
  isPublic: Boolean @search
  access: [ACL]
}

Next, we want to enable a group address book so that users can share either a contact with another user and also share a Contact with a group of users. For this we will add a “Group” type and add a isType on the Contact. This then enables a Contact to represent an individual or a group/organization. We then want to link Groups to Contacts so that we can grant access to groups through contacts.

type Group {
  slug: String! @id
  isContact: Contact!
  hasGrantedRights: [AccessRight]
}

type AccessRight {
  id: ID!
  name: AccessRights @search
  forContact: Contact!
  forGroup: Group! @hasInverse (field: hasGrantedRight)
}

enum AccessRights {
  isAdmin #group admin
  canViewContact
  canAddContact
  canEditContact
  canDeleteContact
  canViewPhone
  canEdit Phone
  canAddPhone
  canDeletePhone
  #more as needed
}

type Contact {
  #continuing from above
  isType: ContactType! @search
  isGroup: Group @hasInverse(field: isContact)
}

enum ContactType {
  PERSON
  ORG
  GROUP
}

That wraps up our schema for now. There are some upcoming things to Dgraph such as Unions and auth on interfaces that will simplify some of what we will do in this series. Take a moment to look back at what we have before moving on. A User can be of either USER or ADMIN type and must have a linked Contact. A Contact must be either a PERSON, ORG, or GROUP, and can have Phone numbers. Access to Contacts will be controlled through the isPublic flag and the list of granted access. Access is granted with either VIEWER, MODERATOR, or OWNER level and is granted to a Contact that is either a User or a Group. Groups have additional rights granted to Users through their linked Contact to granular control what powers members of the Group have.

In the next part, we will add rules to this schema to start restricting access which is commonly called Authorization.


Anthony Master, Author

Anthony has been in the web development realm since 2007. Having a Bachelors of Theology in Missions, which is uncommon in this field, he has received no formal education for Computer Science. He has learned most things the hard way which sometimes has its benefits. Anthony’s passion has brought him to developing a web application serving Churches, Missionaries, and other ministries. Having arrived at a crossroads where relational databases were not able to handle their workflow, they decided to use Dgraph being an early adopter of the GraphQL endpoint and Slash.