Putting it All Together - Dgraph Authentication, Authorization, and Granular Access Control (PART 1) - Dgraph Blog

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.

ReferencesTop image: Chandra X-ray: A Crab Nebula Walks Through Time
![](upload://7TCCyiWI20Cujex6mHOBp3qL6Bb.jpeg) 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.


This is a companion discussion topic for the original entry at https://dgraph.io/blog/post/putting-it-all-together-part1/
6 Likes

True! Many Thanks @amaster507 for your engagement and support!

I would like to better understand the relationship between the DQL schemas ( set via /alter ) and the GraphQL schemas ( set via /admin/schema ). It seems that I can add schemas with the same entity type to both DQL and GraphQL, but queries via GraphQL don’t find the entities that are added via DQL… Is this by design?

I can of course add a custom DQL query to the GraphQL schema and then the entities are returned - but that seems redundant… Are entities added via GraphQL and DQL always somehow separate (even if they have the same type Name) ?

When a schema is added using graphql /admin/schema , the types are stored with the same name as DQL schema.
To check by what name a schema has been added in DQL, you may do the following.

  1. Add a schema using graphql /admin/schema .
  2. Do the following query on DQL, possibly using ratel:
schema {
}
  1. The schema added using graphql /admin/schema will now be visible as output of the DQL schema query.

Similarly any entities added using graphql endpoints could be accessed using DQL, provided proper field names have been given. I am not sure why you are not able to find the entities using DQL.
Can you share more details about the queries which you are trying.

I think it is because types that are added

are prefaced with the type name, eg:
Person.friend
rather than
friend

i also recently asked another question about this on one of the docs pages

Yes, the field of a type get prefaced with the type name. Name of types remains the same.

Great Article Anthony. When should we expect the other 3 parts?

1 Like

Thank you @garhbod, I am not exactly sure of the release schedule. @zhenni, do you have the schedule?

@amaster507, I just lined up your next one to go out tomorrow :slight_smile:

Sorry, there was too longer break between first and second. Let’s get the third (and fourth?) out next week.

2 Likes

Hello. I got confused with predicate of “forContact” within type AccessRight. Since the Access are determined by the Group (Group also have the isContact predicate, I think there shoud be isContact: [Contact] instead of isContact: Contact), why AccessRight still hold the forContact predicate.

I find through all part 4, but found no use case of these Type.
Thanks.

In my use case, a Contact can be an individual, business, organization, school, class, etc. So in the Group isContact connects the singular Contact that is the equivalence of the Group. For example a group might be a School and another group might be a specific class of the school. This is a 1:1 relationship. Every group represents a singular Contact (usually not an individual type).

The AccessRight forContact connects which contact has that specific access right within the group. My use case is for each access right to be specific to a singular contact. This makes it so changing one access right is only changing rights for one connected contact which usually represents an individual user.

My use case potentially will support groups within groups and this lays the foundation for this framework with some more rule modifications. For instance a group could be a school and a another group could be a class. The class has access rights within the school and users in the class can potentially inherit rights through the class to the school. This would allow multiple classes and the school as a whole to collaborate on something collectively without needing to specifically grant every user in every class specific access rights within the parent group, just grant the classes.

I didn’t take this example use case in the articles to its fullest potential and if you want to model it differently then go for it, as this was just some content to get people thinking on how it all flows together.

2 Likes

This example is very good for beginners, because it put many pieces of knowledge together. I spent more than a week studying it and I learned a lot of knowledge. It’s worth it.

I try to rewrite you case with Interface(put the Group and User into same Interface), but I give up it, becuase Dgraph didn’t support filter on Interface neither mutation on Interface, except Query.
I also try rewrite it with Union, but found that Dgraph didn’t support @hasInverse Directive on Union.

So I give up them both. I want to use your case with minor modification, I think the Type Contact should be divided into two Type. One hold the contact information, such as phone, name, etc.
the other hold an individual, business, organization, school, class, etc, it will make this schema more clear for reading.

This visual diogram base on your schema.

2 Likes