Skip to main content

Modeling Entitlements for a System with Auth0 FGA

note
Auth0 Fine Grained Authorization (FGA) is the early-stage product we are building at Auth0 to solve fine-grained authorization at scale. Sign up for the Developer Community Preview to try it out, and join our Discord community if you are interested in learning more about our plans.

Please note that at this point in time, it is not considered production-ready and does not come with any SLAs; availability and uptime are not guaranteed. Limitations of Auth0 FGA during the Developer Community Preview can be found here.

This tutorial explains how to model entitlements for a platform like GitHub using Auth0 FGA.

What you will learn
  • How to model an entitlement use case in Auth0 FGA
  • How to start with a given set of requirements and scenarios and iterate on the Auth0 FGA model until those requirements are met

Entitlements

Explore the Entitlements sample on the Auth0 FGA Playground

Before You Start

In order to understand this guide correctly you must be familiar with some Auth0 Fine Grained Authorization (FGA) concepts and know how to develop the things that we will list below.

Auth0 FGA Concepts

It would be helpful to have an understanding of some concepts of Auth0 FGA before you start.

Modeling Object-to-Object Relationships

You need to know how to create relationships between objects and how that might affect a user's relationships to those objects. Learn more →

Used here to indicate that members of an org are subscriber members of the plan the org is subscriber to, and subscriber members of a plan get access to all the plan's features.

Direct Relationships

You need to know how to disallow granting direct relation to an object and requiring the user to have a relation with another object that would imply a relation with the first one. Learn more →

Used here to indicate that "access" to a feature cannot be directly granted to a user, but is implied through the users organization subscribing to a plan that offers that feature.

Concepts & Configuration Language

What You Will You Be Modeling

In many product offerings, the features are behind multiple tiers. In this tutorial, you will build an authorization model for a subset of GitHub's entitlements (detailed below) using Auth0 Fine Grained Authorization (FGA). You will use some scenarios to validate the model.

GitHub Pricing Plan

At their core, entitlements is just asking: does a user X have access to feature Y? In GitHub's case for example, they have a concept called "Draft Pull Requests". Once the user loads the Pull Request page, the frontend needs to know whether it can show the "Draft Pull Request" option, as in it needs to know: "Does the current user have access to feature Draft Pull Request?".

GitHub PR Page with Draft Pull Request GitHub PR Page without Draft Pull Request

Note: For brevity, this tutorial will not model all of GitHub entitlements. Instead, it will focus on modeling for the scenarios outlined below

Requirements

You will model an entitlement system similar to GitHub's, focusing on a few scenarios.

GitHub has 3 plans: "Free", "Team" and "Enterprise", with each of them offering several features. The higher-priced plans include all the features of the lower priced plans. You will be focusing on a subset of the features offered.

A summary of GitHub's entitlement system:

  • Free
    • Issues
  • Team
    • Everything from the free plan
    • Draft Pull Requests
  • Enterprise
    • Everything from the team plan
    • SAML Single Sign-On

Defined Scenarios

Use the following scenarios to be able to validate whether the model of the requirements is correct.

  • Take these three organizations

    • Alpha Beta Gamma (alpha), a subscriber on the free plan
    • Bayer Water Supplies (bayer), a subscriber on the team plan
    • Cups and Dishes (cups), a subscriber on the enterprise plan
  • Take these three users

    • Anne, member of Alpha Beta Gamma
    • Beth, member of Bayer Water Supplies
    • Charles, member of Cups and Dishes

Image showing requirements

By the end of this tutorial, you should be able to query Auth0 FGA with queries like:

  • Anne has access to Issues (expecting yes)
  • Anne has access to Draft Pull Requests (expecting no)
  • Anne has access to Single Sign-on (expecting no)
  • Beth has access to Issues (expecting yes)
  • Beth has access to Draft Pull Requests (expecting yes)
  • Beth has access to Single Sign-on (expecting no)
  • Charles has access to Issues (expecting yes)
  • Charles has access to Draft Pull Requests (expecting yes)
  • Charles has access to Single Sign-on (expecting yes)

Modeling Entitlements for GitHub

01. Building the Initial Authorization Model and Relationship Tuples

In this tutorial you are going to take a different approach to previous tutorials. You will start with a simpleauthorization model, add relationship tuples to represent some sample scenarios, and iterate until those scenarios return the results you expect.

In the scenarios outlined above, you have organizations, plans and features.

Similar to the example above, start with a basic listing of the types and their relations:

  • A feature has a plan associated to it, we'll call the relation between them associated_plan
  • A plan has an organization as a subscriber to it
  • An organization has users as members
type feature
relations
define associated_plan as self
type plan
relations
define subscriber as self
type organization
relations
define member as self

02. Populating the Relationship Tuples

Now you can add the relationship tuples to represent these relationships mentioned in the requirements and scenarios sections:

The relations between the features and plans are as follows:

Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');

// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});

await fgaClient.write({
writes: {
tuple_keys: [
// the free plan is the associated plan of the issues feature
{ user: 'plan:free', relation: 'associated_plan', object: 'feature:issues'},
// the team plan is the associated plan of the issues feature
{ user: 'plan:team', relation: 'associated_plan', object: 'feature:issues'},
// the team plan is the associated plan of the draft pull requests feature
{ user: 'plan:team', relation: 'associated_plan', object: 'feature:draft_prs'},
// the enterprise plan is the associated plan of the issues feature
{ user: 'plan:enterprise', relation: 'associated_plan', object: 'feature:issues'},
// the enterprise plan is the associated plan of the draft pull requests feature
{ user: 'plan:enterprise', relation: 'associated_plan', object: 'feature:draft_prs'},
// the enterprise plan is the associated plan of the SAML Single Sign-on feature
{ user: 'plan:enterprise', relation: 'associated_plan', object: 'feature:sso'}
]
}
});

The relations between the plans and the organizations are as follows:

Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');

// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});

await fgaClient.write({
writes: {
tuple_keys: [
// the Alpha Beta Gamma organization is a subscriber of the free plan
{ user: 'organization:alpha', relation: 'subscriber', object: 'plan:free'},
// the Bayer Water Supplies organization is a subscriber of the team plan
{ user: 'organization:bayer', relation: 'subscriber', object: 'plan:team'},
// the Cups and Dishes organization is a subscriber of the enterprise plan
{ user: 'organization:cups', relation: 'subscriber', object: 'plan:enterprise'}
]
}
});

The relations between the organizations and the users are as follows:

Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');

// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});

await fgaClient.write({
writes: {
tuple_keys: [
// anne is a member of the Alpha Beta Gamma organization
{ user: 'anne', relation: 'member', object: 'organization:alpha'},
// beth is a member of the Bayer Water Supplies
{ user: 'beth', relation: 'member', object: 'organization:bayer'},
// charles is a member of the Cups and Dishes organization
{ user: 'charles', relation: 'member', object: 'organization:cups'}
]
}
});

So far you have given Auth0 FGA a representation of the current state of your system's relationships. You will keep iterating and updating the authorization model until the results of the queries match what you expect.

caution

In production, it is highly recommended to use unique, immutable identifiers. Names are used in this article to make it easier to read and follow. For example, the relationship tuple indicating that anne is a member of organization:alpha could be written as:

  • user: 2b4840f2-7c9c-42c8-9329-911002051524
  • relation: member
  • object: project:52e529c6-c571-4d5c-b78a-bc574cf98b54

Verification

Now that you have some data, you can start using it to ask is ${USER} related to ${OBJECT} as ${RELATION}?

First, you will check if anne is a member of organization:alpha. This is one of the relationship tuples you previously added, you will make sure Auth0 FGA can detect a relation in this case.

Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');

// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});

// Run a check
const { allowed } = await fgaClient.check({
tuple_key: {
user: 'anne',
relation: 'member',
object: 'organization:alpha',
},});

// allowed = true

Querying for relationship tuples that you fed into Auth0 Fine Grained Authorization (FGA) earlier should work, try a few before proceeding to make sure everything is working well.

Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');

// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});

// Run a check
const { allowed } = await fgaClient.check({
tuple_key: {
user: 'anne',
relation: 'member',
object: 'organization:bayer',
},});

// allowed = false
Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');

// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});

// Run a check
const { allowed } = await fgaClient.check({
tuple_key: {
user: 'organization:bayer',
relation: 'member',
object: 'plan:team',
},});

// allowed = true
Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');

// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});

// Run a check
const { allowed } = await fgaClient.check({
tuple_key: {
user: 'plan:free',
relation: 'associated_plan',
object: 'feature:issues',
},});

// allowed = true

03. Updating the Authorization Model

You are working towards Auth0 FGA returning the correct answer when you query whether anne has access to feature:issues. It won't work yet, but you will keep updating your configuration to reach that goal.

To start, try to run that query on is anne related to feature:issues as access?

Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');

// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});

// Run a check
const { allowed } = await fgaClient.check({
tuple_key: {
user: 'anne',
relation: 'access',
object: 'feature:issues',
},});

// allowed = undefined

The Auth0 Fine Grained Authorization (FGA) service is returning that the query tuple is invalid. That is because you are asking for relation as access, but that relation is not in the configuration of the feature type!

Add it now. Like so:

type feature
relations
define associated_plan as self
define access as self
info

access relation was added to the configuration of the feature type.

note

In this tutorial, you will find the phrases direct relationship and implied relationship.

A direct relationship R between user X and object Y means the relationship tuple (user=X, relation=R, object=Y) exists, and the Auth0 FGA authorization model for that relation allows this direct relationship (by use of self).

An implied relationship R exists between user X and object Y if user X is related to an object Z that is in direct or implied relationship with object Y, and the Auth0 FGA authorization model allows it.

The resulting updated configuration would be:

type feature
relations
define associated_plan as self
define access as self
type plan
relations
define subscriber as self
type organization
relations
define member as self

Adding modeling pattern of parent-child objects

Now we can ask the following query: is anne related to feature:issues as access? again.

Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');

// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});

// Run a check
const { allowed } = await fgaClient.check({
tuple_key: {
user: 'anne',
relation: 'access',
object: 'feature:issues',
},});

// allowed = false

So far so good. Auth0 FGA understood your query, but said that no relation exists. That is because according to the configuration provided so far, there is no access relation between anne and feature:issues.

We can also try to query is organization:alpha related to feature:issues as access? and we see that there is no relationship.

Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');

// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});

// Run a check
const { allowed } = await fgaClient.check({
tuple_key: {
user: 'organization:alpha',
relation: 'access',
object: 'feature:issues',
},});

// allowed = false

If you have already completed some of the other tutorials you might have encountered the modeling pattern of parent-child objects which is modeled as such:

    define viewer as all_objects_viewer from parent
info

With this, when asked to check a user's viewer relationship with the object, Auth0 Fine Grained Authorization (FGA) will:

  1. Read all relationship tuples of users related to this particular object as relation parent
  2. For each relationship tuple, return all usersets that have all_objects_viewer relation to the objects in those relationship tuples
  3. If the user is in any of those usersets, return yes, as the user is a viewer on this object. In other words, users related as all_objects_viewer to any of this object's parents are related as viewer to this object.

If you want to give all subscribers on a plan access to a feature, you can do it like so:

type feature
relations
define associated_plan as self
define access as self or subscriber from associated_plan
info

users related to feature as access are the union of (any of):

* the set of users with a direct `access` relation
* the set of users related to the `associated_plan` as `subscriber` (the feature's associated plans' subscribers)

So everyone who has direct access, as well as the subscribers of the associated plan

That would mean that in order for an object to have an access relation to a feature y, there needs to be either a:

  • a direct relationship via a relationship tuple: e.g. { "user": "x", "relation": "access", "object": "feature:y" }
  • subscriber relationship with another object related to x associated_plan: e.g. { "user": "x", "relation": "subscriber", "object": "plan:z" } { "user": "plan:z", "relation": "associated_plan", "object": "feature:y" }

That brings you close. That will allow you to grant organizations access to the feature (as organizations have a subscriber relation with the plan).

One way forward would be to add a direct access relation between a user and a feature e.g. { "user": "anne", "relation": "access", "object": "feature:y" } whenever the organization anne is subscribed to a plan, or the organization anne is in subscribes to a new plan. But there are several downsides to this:

  • Your application layer now needs to worry about computing this relationship. Instead of letting Auth0 FGA figure this all out, the app layer needs to do the checks whenever a user is being added or removed
  • If an organization changes its subscription, your application layer has to loop through all the users and update their access relationships to features accordingly

Later in this tutorial, you will remove the possibility of having a direct access relation completely, but for now you will make sure the changes to the store you have made so far are working.

Replace all the existing code you had previously with the updated authorization model from the below snippet.

type feature
relations
define associated_plan as self
define access as self or subscriber from associated_plan
type plan
relations
define subscriber as self
type organization
relations
define member as self

Now we can ask following query: is organization:alpha related to feature:issues as access? again.

Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');

// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});

// Run a check
const { allowed } = await fgaClient.check({
tuple_key: {
user: 'organization:alpha',
relation: 'access',
object: 'feature:issues',
},});

// allowed = true

You will notice that Auth0 FGA now did find a relation, as organization:alpha is a subscriber to plan:free which has an associated_plan relation to feature:issues. From that and the authorization model you updated above, Auth0 FGA deduced that organization:alpha has an implied access relation to feature:issues.

That is good, but you want to be able to ask is anne related to feature:issues as access?, not is organization:alpha related to feature:issues as access?. As in, you want the subscriber members to have access to the feature, not the subscriber itself.

In order to do that, you will add a relation on the plan, that indicates that all members of an organization subscribed to it, have a subscriber_member relation to the plan. And you can modify the change you did above to give implied access to the subscriber_member instead of the subscriber. Like so:

type feature
relations
define associated_plan as self
define access as self or subscriber_member from associated_plan
type plan
relations
define subscriber as self
define subscriber_member as member from subscriber
type organization
relations
define member as self
info

Notice that subscriber has been updated to subscriber_member in the access relation of the feature type.

Under the plan type, in order for someone to have a subscriber_member relation to the plan, they have to be related as a member to the object related as a subscriber to the plan (as in they have to be a member of on of the plan's subscribers).

Now ask the following query: is anne related to feature:issues as access?

Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');

// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});

// Run a check
const { allowed } = await fgaClient.check({
tuple_key: {
user: 'anne',
relation: 'access',
object: 'feature:issues',
},});

// allowed = true

Disallow direction relationship

So far, with just a Auth0 FGA authorization model, and the initial relationship tuples indication the relations you know, you configured Auth0 Fine Grained Authorization (FGA) to give you the correct response.

Earlier on, the idea of not allowing a direct access relation between a user and a feature was discussed, e.g. adding a relationship tuple like { "user": "anne", "relation": "access", "object": "feature:y" }. You will remove it now.

To disallow a direct relationship, you need to remove self. The following snippet:

type feature
relations
define associated_plan as self
define access as self or subscriber_member from associated_plan

becomes

type feature
relations
define associated_plan as self
define access as subscriber_member from associated_plan

With this change, even if your app layer added the following relationship tuple:

  • { "user": "anne", "relation": "access", "object": feature:issues }

a subsequent check for is anne related to feature:issues as access? would return no relation. The only way for a relation to exist is if the following three relationship tuples do:

  • { "user": "anne", "relation": "member", "object": "organization:z" }
  • { "user": "organization:z", "relation": "subscriber", "object": "plan:y" }
  • { "user": "play:y", "relation": "associated_plan", "object": "feature:issues" }

Verification

Ensure that your authorization model matches the one below

type feature
relations
define associated_plan as self
define access as subscriber_member from associated_plan
type plan
relations
define subscriber as self
define subscriber_member as member from subscriber
type organization
relations
define member as self

You will now verify that the configuration is correct by running checks for all the scenarios mentioned at the beginning of the tutorial:

  • Anne has access to Issues (expecting yes)
  • Anne has access to Draft Pull Requests (expecting no)
  • Anne has access to Single Sign-on (expecting no)
  • Beth has access to Issues (expecting yes)
  • Beth has access to Draft Pull Requests (expecting yes)
  • Beth has access to Single Sign-on (expecting no)
  • Charles has access to Issues (expecting yes)
  • Charles has access to Draft Pull Requests (expecting yes)
  • Charles has access to Single Sign-on (expecting yes)
Initialize the SDK
// FGA_ENVIRONMENT can be "us" (default if not set) for Developer Community Preview or "playground" for the Playground API
// import the SDK
const { Auth0FgaApi } = require('@auth0/fga');

// Initialize the SDK
const fgaClient = new Auth0FgaApi({
environment: process.env.FGA_ENVIRONMENT,
storeId: process.env.FGA_STORE_ID,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
});

// Run a check
const { allowed } = await fgaClient.check({
tuple_key: {
user: 'anne',
relation: 'access',
object: 'feature:issues',
},});

// allowed = true

Try to verify for the other user, object and relation combinations as listed below.

UserObjectRelationQueryRelation?
annefeature:issuesaccessis anne related to feature:issues as access?Yes
annefeature:draft_prsaccessis anne related to feature:draft_prs as access?No
annefeature:ssoaccessis anne related to feature:sso as access?No
bethfeature:issuesaccessis beth related to feature:issues as access?Yes
bethfeature:draft_prsaccessis beth related to feature:draft_prs as access?Yes
bethfeature:ssoaccessis beth related to feature:sso as access?No
charlesfeature:issuesaccessis charles related to feature:issues as access?Yes
charlesfeature:draft_prsaccessis charles related to feature:draft_prs as access?Yes
charlesfeature:ssoaccessis charles related to feature:sso as access?Yes

Summary

In this tutorial, you learned:

  • to model entitlements for a system in Auth0 Fine Grained Authorization (FGA)
  • how to start with a set of requirements and scenarios and iterate on the Auth0 FGA authorization model until the checks match the expected scenarios
  • how to model parent-child relationships to indicate that a user having a relationship with a certain object implies having a relationship with another object in Auth0 FGA
  • how to use the union operator condition to indicate multiple possible paths for a relationship between two objects to be computed
  • using the direct relationship keyword (self) in a Auth0 FGA authorization model, and how to block direct relationships by removing it

Entitlements

Explore the Entitlements sample on the Auth0 FGA Playground

Upcoming tutorials will dive deeper into Auth0 FGA, introducing concepts that will improve on the model you built today, and tackling different permission systems, with other relations and requirements that need to be met.

Have Feedback?

Join us on the Discord community if you have any questions or suggestions.