Skip to main content

Modeling GitHub permissions 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 GitHub's Organization permission model using Auth0 FGA. This article from the GitHub docs has links to all other articles we are going to be exploring in this document.

What you will learn
  • Indicate relationships between a group of users and an object. See Modeling User Groups for more details.
    Used here to indicate that all members of an organization are repository admins on the organization.
  • Modeling concentric relationship to have a certain relation on an object imply another relation on the same object. See Modeling Concepts: Concentric Relationships for more.
    Used here to indicate that maintainers of a repository are also writers of that repository.
  • Using the union operator condition to indicate that a user might have a certain relation with an object if they match any of the criteria indicated.
    Used here to indicate that a user can be a reader on a repository, or can have the reader relationship implied through triager.
  • Model parent-child objects to indicate that a user having a relationship with a certain object implies having a relationship with another object in Auth0 FGA.
    Used here to indicate that a repository admin on a GitHub organization, is an admin on all repositories that organization owns.

GitHub

Explore the GitHub 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 Concentric Relationships

You need to know how to update the authorization model to allow having nested relations such as all writers are readers. Learn more →

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 users who have repo admin access on an organization, have admin access to all repositories owned by that organization.

Concepts & Configuration Language

What You Will You Be Modeling

GitHub is a system to develop and collaborate on code.

In this tutorial, you will build a subset of the GitHub permission model (detailed below) in Auth0 Fine Grained Authorization (FGA), using some scenarios to validate the model.

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

Requirements

GitHub's permission model is represented in their documentation.

In this tutorial, you will be focusing on a subset of these permissions.

Requirements:

  • Users can be admins, maintainers, writers, triagers or readers of repositories (each level inherits all access of the level lower than it. e.g. admins inherit maintainer access and so forth)
  • Teams can have members
  • Organizations can have members
  • Organizations can own repositories
  • Users can have repository admin access on organizations, and thus have admin access to all repositories owned by that organization

Defined Scenarios

There will be the following users:

  • Anne
  • Beth
  • Charles, a member of the contoso/engineering team
  • Diane, a member of the contoso/protocols team
  • Erik, a member of the contoso org

And these requirements:

  • members of the contoso/protocols team are members of the contoso/engineering team
  • members of the contoso org are repo_admins on the org
  • repo admins on the org are admins on all the repos the org owns

There will be a:

  • contoso/tooling repository, owned by the contoso org and of which Beth is a writer and Anne is a reader and members of the contoso/engineering team are admins

Modeling GitHub's Permissions

01. Permissions for individuals in an org

GitHub has 5 different permission levels for repositories:

Image showing github permission levels

At the end of this section we want to end up with the following permissions represented:

Image showing permissions

To represent permissions in Auth0 Fine Grained Authorization (FGA) we use relations. For repository permissions we need to create the following authorization model:

type repo
relations
define reader as self
define triager as self
define writer as self
define maintainer as self
define admin as self

The Auth0 FGA service determines if a user has access to an object by checking if the user has a relation to that object. Let us examine one of those relations in detail:

type repo
relations
define reader as self
info

Objects of type "repo" have users related to them as "reader" if those users belong to the userset of all users related to the repo as "reader"

If we want to say anne is a reader of repository repo:contoso/tooling we had create this relationship tuple:

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: [
{ user: 'anne', relation: 'reader', object: 'repo:contoso/tooling'}
]
}
});

We can now ask Auth0 FGA "is anne a reader of repository repo:contoso/tooling?"

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: 'reader',
object: 'repo:contoso/tooling',
},});

// allowed = true

We could also say that beth is a writer of the same repository:

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: [
{ user: 'beth', relation: 'writer', object: 'repo:contoso/tooling'}
]
}
});

And ask some questions to Auth0 FGA:

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: 'beth',
relation: 'writer',
object: 'repo:contoso/tooling',
},});

// 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: 'beth',
relation: 'reader',
object: 'repo:contoso/tooling',
},});

// allowed = false

The first reply makes sense but the second one does not. Intuitively, if beth was writer, she was also be a reader. In fact, GitHub explains this in their documentation Showing various Github repo access level

To make Auth0 FGA aware of this "concentric" permission model we need to update our definitions:

type repo
relations
define reader as self or triager
define triager as self or writer
define writer as self or maintainer
define maintainer as self or admin
define admin as self

Let us examine one of those relations in detail:

type repo
relations
define reader as self or triager
info

The users with a reader relationship to a certain object of type "repo" are any of:

  • the "readers": the set of users who are directly related to the repo as a "reader"
  • the "triagers": the set of users who are related to the object as "triager"

With this simple update our model now supports nested definitions and now:

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: 'beth',
relation: 'writer',
object: 'repo:contoso/tooling',
},});

// 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: 'beth',
relation: 'reader',
object: 'repo:contoso/tooling',
},});

// allowed = true

02. Permissions for teams in an org

GitHub also supports creating teams in an organization, adding members to a team and granting teams permissions, rather than individuals.

At the end of this section we want to end up with the following permissions represented:

Image showing permissions

To add support for teams and memberships all we need to do is add this object to the Auth0 FGA authorization model:

type team
relations
define member as self

Let us now create a team, add a member to it and make it an admin of repo:contoso/tooling by adding the following relationship tuples:

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: [
// make charles a member of the contoso/engineering team
{ user: 'charles', relation: 'member', object: 'team:contoso/engineering'},
// make members of contoso/engineering team admins of contoso/tooling
{ user: 'team:contoso/engineering#member', relation: 'admin', object: 'repo:contoso/tooling'}
]
}
});

The last relationship tuple introduces a new Auth0 FGA concept. A userset. When the value of a user is formatted like this type:objectId#relation, Auth0 FGA will automatically expand the userset into all its individual user identifiers:

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: 'charles',
relation: 'admin',
object: 'repo:contoso/tooling',
},});

// allowed = true

03. Permissions for child teams in an org

GitHub also supports team nesting, known as "child teams". Child teams inherit the access permissions of the parent team. Let's say we have a protocols team that is part of the engineering. The simplest way to achieve the aforementioned requirement is just adding this relationship tuple:

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: [
{ user: 'team:contoso/protocols#member', relation: 'member', object: 'team:contoso/engineering'}
]
}
});

which says that members of protocols are members of engineering.

Note: this is enough and valid for our current requirements, and for other read cases allows determining members of the direct team vs sub teams as the latter come from team:contoso/protocols#member. If the #member relation should not be followed for use cases a different approach could be taken.

We can now add a member to the protocols team and check that they are admins of the tooling repository.

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: [
// make diane a member of the contoso/protocols team
{ user: 'diane', relation: 'member', object: 'team:contoso/protocols'}
]
}
});
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: 'diane',
relation: 'admin',
object: 'repo:contoso/tooling',
},});

// allowed = true

At the end of this section ended with the following permissions represented:

Image showing permissions

04. Base permissions for org members

In GitHub, "you can can set base permissions that apply to all members of an organization when accessing any of the organization's repositories". For our purposes this means that if:

  • and contoso has a repository tooling
  • and contoso has configured base permission to be "write"

then erik has write permissions to tooling.

Let us model that!

At the end of this section we want to end up with the following permissions represented:

We need to introduce the notion of organization as a type, user organization membership and repository ownership as a relation. - It is worth calling that before this addition we were able to represent almost the entire GitHub repo permissions without adding the notion of organization to Auth0 FGA. Identifiers for users, repositories and teams were all that was necessary. Let us add support for organizations and membership. Hopefully this feels familiar by now:

type org
relations
define member as self

And support for repositories having owners:

type repo
relations
define reader as self or triager
define triager as self or writer
define writer as self or maintainer
define maintainer as self or admin
define admin as self
define owner as self
info

Note the added "owner" relation, indicating that organizations can own repositories.

We can now make Erik a member of contoso and make contoso own contoso/tooling:

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: [
{ user: 'erik', relation: 'member', object: 'org:contoso'},
{ user: 'org:contoso', relation: 'owner', object: 'repo:contoso/tooling'}
]
}
});

What we still lack is the ability to create "default permissions" for the organization and have those be considered when determining if a user has a particular relation to a repository. Let's start with the simplest case admin. We want to say that a user is a admin of a repo if either:

  • [done] they have a repo admin relation (directly or through team membership)
  • [pending] their organization is configured with repo_admin as the base permission

We need a way to consider the organization members, not just direct relations to the repo when getting a check for:

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: 'erik',
relation: 'admin',
object: 'repo:contoso/tooling',
},});

// allowed = undefined

More details on this technique can be found in the section Modeling Parent-Child Objects.

We express it like this:

    define admin as self or repo_admin from owner
info

The users with an admin relationship to a certain object of type "repo" are any of:

  • the "admins": the set of users who are directly related to the repo as an "admin"
  • the "repository admins of the org that owns the repo": from the objects who are related to the doc as parent, return the sets of users who are related to those objects as "repo_admin"

What the added section is doing is:

  1. read all relationship tuples related to repo:contoso/tooling as parent which returns:

    [{ "object": "repo:contoso/tooling", "relation": "parent", "user": "org:contoso" }]

  2. for each relationship tuple read, return all usersets that match the following, returning tuples of shape:

    { "object": "org:contoso", "repo_admin", "user": ??? }

What should the users in those relationship tuples with ??? be?

  • Well:
    • If the base permission for org contoso is repo_admin then it should be org:contoso#member.
    • If the base permission for org contoso is NOT repo_admin, then it should be empty (no relationship tuple).
  • Whenever the value of this dropdown changes: Selecting new permission level from base permissions drop-down
    • Delete the previous relationship tuple and create a new one:
      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: [
      { user: 'org:contoso#member', relation: 'repo_admin', object: 'org:contoso'}
      ]
      }
      });

The updated authorization model looks like this:

type repo
relations
define admin as self or admin from owner
define maintainer as self or admin
define writer as self or maintainer or writer from owner
define triager as self or writer
define reader as self or triager or reader from owner
define owner as self
type org
relations
define owner as self
define repo_admin as self

Summary

GitHub has a number of other permissions. You have organization billing managers, users that can manage specific apps, etc. We might explore those in the future, but hopefully this blog post has shown you how you could represent those cases using Auth0 Fine Grained Authorization (FGA).

GitHub

Explore the GitHub sample on the Auth0 FGA Playground

Have Feedback?

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