Skip to main content

Modeling GitHub permissions with Okta FGA

This tutorial explains how to model GitHub's Organization permission model using Okta 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 Okta 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 Okta FGA Playground

Before You Start

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

Okta FGA Concepts

It would be helpful to have an understanding of some concepts of Okta 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 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 Okta 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 Okta Fine Grained Authorization (FGA) we use relations. For repository permissions we need to create the following authorization model:

model
schema 1.1

type user

type repo
relations
define reader: [user]
define triager: [user]
define writer: [user]
define maintainer: [user]
define admin: [user]

The Okta 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:

model
schema 1.1

type user

type repo
relations
define reader: [user]
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 create this relationship tuple:

Initialize the SDK
// Checkout the "How to Setup the SDK Client" page for more details.
const { CredentialsMethod, OpenFgaClient } = require('@openfga/sdk'); // OR import { CredentialsMethod, OpenFgaClient } from '@openfga/sdk';

// Ensure the environment variables are set
// FGA_API_URL = 'https://api.us1.fga.dev' // 'https://api.eu1.fga.dev' for EU and 'https://api.au1.fga.dev' for AU
// FGA_STORE_ID = 'YOUR_STORE_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_MODEL_ID = 'YOUR_MODEL_ID' - optional, can be overridden per request, helps reduce latency
// FGA_API_TOKEN_ISSUER = 'fga.us.auth0.com'
// FGA_API_AUDIENCE = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
// FGA_CLIENT_ID = 'YOUR_CLIENT_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page

const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID,
credentials: { // Credentials are not needed if connecting to the Playground API
method: CredentialsMethod.ClientCredentials,
config: {
apiTokenIssuer: process.env.FGA_API_TOKEN_ISSUER,
apiAudience: process.env.FGA_API_AUDIENCE,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
},
},
});

await fgaClient.write({
writes: [
{"user":"user:anne","relation":"reader","object":"repo:contoso/tooling"}
],
}, {
authorization_model_id: "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
});

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

Initialize the SDK
// Checkout the "How to Setup the SDK Client" page for more details.
const { CredentialsMethod, OpenFgaClient } = require('@openfga/sdk'); // OR import { CredentialsMethod, OpenFgaClient } from '@openfga/sdk';

// Ensure the environment variables are set
// FGA_API_URL = 'https://api.us1.fga.dev' // 'https://api.eu1.fga.dev' for EU and 'https://api.au1.fga.dev' for AU
// FGA_STORE_ID = 'YOUR_STORE_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_MODEL_ID = 'YOUR_MODEL_ID' - optional, can be overridden per request, helps reduce latency
// FGA_API_TOKEN_ISSUER = 'fga.us.auth0.com'
// FGA_API_AUDIENCE = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
// FGA_CLIENT_ID = 'YOUR_CLIENT_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page

const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID,
credentials: { // Credentials are not needed if connecting to the Playground API
method: CredentialsMethod.ClientCredentials,
config: {
apiTokenIssuer: process.env.FGA_API_TOKEN_ISSUER,
apiAudience: process.env.FGA_API_AUDIENCE,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
},
},
});

// Run a check
const { allowed } = await fgaClient.check({
user: 'user:anne',
relation: 'reader',
object: 'repo:contoso/tooling',
}, {
authorization_model_id: '1uHxCSuTP0VKPYSnkq1pbb1jeZw',
});

// allowed = true

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

Initialize the SDK
// Checkout the "How to Setup the SDK Client" page for more details.
const { CredentialsMethod, OpenFgaClient } = require('@openfga/sdk'); // OR import { CredentialsMethod, OpenFgaClient } from '@openfga/sdk';

// Ensure the environment variables are set
// FGA_API_URL = 'https://api.us1.fga.dev' // 'https://api.eu1.fga.dev' for EU and 'https://api.au1.fga.dev' for AU
// FGA_STORE_ID = 'YOUR_STORE_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_MODEL_ID = 'YOUR_MODEL_ID' - optional, can be overridden per request, helps reduce latency
// FGA_API_TOKEN_ISSUER = 'fga.us.auth0.com'
// FGA_API_AUDIENCE = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
// FGA_CLIENT_ID = 'YOUR_CLIENT_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page

const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID,
credentials: { // Credentials are not needed if connecting to the Playground API
method: CredentialsMethod.ClientCredentials,
config: {
apiTokenIssuer: process.env.FGA_API_TOKEN_ISSUER,
apiAudience: process.env.FGA_API_AUDIENCE,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
},
},
});

await fgaClient.write({
writes: [
{"user":"user:beth","relation":"writer","object":"repo:contoso/tooling"}
],
}, {
authorization_model_id: "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
});

And ask some questions to Okta FGA:

Initialize the SDK
// Checkout the "How to Setup the SDK Client" page for more details.
const { CredentialsMethod, OpenFgaClient } = require('@openfga/sdk'); // OR import { CredentialsMethod, OpenFgaClient } from '@openfga/sdk';

// Ensure the environment variables are set
// FGA_API_URL = 'https://api.us1.fga.dev' // 'https://api.eu1.fga.dev' for EU and 'https://api.au1.fga.dev' for AU
// FGA_STORE_ID = 'YOUR_STORE_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_MODEL_ID = 'YOUR_MODEL_ID' - optional, can be overridden per request, helps reduce latency
// FGA_API_TOKEN_ISSUER = 'fga.us.auth0.com'
// FGA_API_AUDIENCE = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
// FGA_CLIENT_ID = 'YOUR_CLIENT_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page

const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID,
credentials: { // Credentials are not needed if connecting to the Playground API
method: CredentialsMethod.ClientCredentials,
config: {
apiTokenIssuer: process.env.FGA_API_TOKEN_ISSUER,
apiAudience: process.env.FGA_API_AUDIENCE,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
},
},
});

// Run a check
const { allowed } = await fgaClient.check({
user: 'user:beth',
relation: 'writer',
object: 'repo:contoso/tooling',
}, {
authorization_model_id: '1uHxCSuTP0VKPYSnkq1pbb1jeZw',
});

// allowed = true
Initialize the SDK
// Checkout the "How to Setup the SDK Client" page for more details.
const { CredentialsMethod, OpenFgaClient } = require('@openfga/sdk'); // OR import { CredentialsMethod, OpenFgaClient } from '@openfga/sdk';

// Ensure the environment variables are set
// FGA_API_URL = 'https://api.us1.fga.dev' // 'https://api.eu1.fga.dev' for EU and 'https://api.au1.fga.dev' for AU
// FGA_STORE_ID = 'YOUR_STORE_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_MODEL_ID = 'YOUR_MODEL_ID' - optional, can be overridden per request, helps reduce latency
// FGA_API_TOKEN_ISSUER = 'fga.us.auth0.com'
// FGA_API_AUDIENCE = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
// FGA_CLIENT_ID = 'YOUR_CLIENT_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page

const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID,
credentials: { // Credentials are not needed if connecting to the Playground API
method: CredentialsMethod.ClientCredentials,
config: {
apiTokenIssuer: process.env.FGA_API_TOKEN_ISSUER,
apiAudience: process.env.FGA_API_AUDIENCE,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
},
},
});

// Run a check
const { allowed } = await fgaClient.check({
user: 'user:beth',
relation: 'reader',
object: 'repo:contoso/tooling',
}, {
authorization_model_id: '1uHxCSuTP0VKPYSnkq1pbb1jeZw',
});

// 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 Okta FGA aware of this "concentric" permission model we need to update our definitions:

model
schema 1.1

type user

type repo
relations
define reader: [user] or triager
define triager: [user] or writer
define writer: [user] or maintainer
define maintainer: [user] or admin
define admin: [user]

Let us examine one of those relations in detail:


type repo
relations
define reader: [user] 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
// Checkout the "How to Setup the SDK Client" page for more details.
const { CredentialsMethod, OpenFgaClient } = require('@openfga/sdk'); // OR import { CredentialsMethod, OpenFgaClient } from '@openfga/sdk';

// Ensure the environment variables are set
// FGA_API_URL = 'https://api.us1.fga.dev' // 'https://api.eu1.fga.dev' for EU and 'https://api.au1.fga.dev' for AU
// FGA_STORE_ID = 'YOUR_STORE_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_MODEL_ID = 'YOUR_MODEL_ID' - optional, can be overridden per request, helps reduce latency
// FGA_API_TOKEN_ISSUER = 'fga.us.auth0.com'
// FGA_API_AUDIENCE = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
// FGA_CLIENT_ID = 'YOUR_CLIENT_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page

const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID,
credentials: { // Credentials are not needed if connecting to the Playground API
method: CredentialsMethod.ClientCredentials,
config: {
apiTokenIssuer: process.env.FGA_API_TOKEN_ISSUER,
apiAudience: process.env.FGA_API_AUDIENCE,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
},
},
});

// Run a check
const { allowed } = await fgaClient.check({
user: 'user:beth',
relation: 'writer',
object: 'repo:contoso/tooling',
}, {
authorization_model_id: '1uHxCSuTP0VKPYSnkq1pbb1jeZw',
});

// allowed = true
Initialize the SDK
// Checkout the "How to Setup the SDK Client" page for more details.
const { CredentialsMethod, OpenFgaClient } = require('@openfga/sdk'); // OR import { CredentialsMethod, OpenFgaClient } from '@openfga/sdk';

// Ensure the environment variables are set
// FGA_API_URL = 'https://api.us1.fga.dev' // 'https://api.eu1.fga.dev' for EU and 'https://api.au1.fga.dev' for AU
// FGA_STORE_ID = 'YOUR_STORE_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_MODEL_ID = 'YOUR_MODEL_ID' - optional, can be overridden per request, helps reduce latency
// FGA_API_TOKEN_ISSUER = 'fga.us.auth0.com'
// FGA_API_AUDIENCE = 'https://api.us1.fga.dev/' // 'https://api.eu1.fga.dev/' for EU and 'https://api.au1.fga.dev/' for AU
// FGA_CLIENT_ID = 'YOUR_CLIENT_ID' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page
// FGA_CLIENT_SECRET = 'YOUR_CLIENT_SECRET' - Get this from your store settings in the dashboard, refer to the "How to get your API Keys" page

const fgaClient = new OpenFgaClient({
apiUrl: process.env.FGA_API_URL,
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID,
credentials: { // Credentials are not needed if connecting to the Playground API
method: CredentialsMethod.ClientCredentials,
config: {
apiTokenIssuer: process.env.FGA_API_TOKEN_ISSUER,
apiAudience: process.env.FGA_API_AUDIENCE,
clientId: process.env.FGA_CLIENT_ID,
clientSecret: process.env.FGA_CLIENT_SECRET,
},
},
});

// Run a check
const { allowed } = await fgaClient.check({
user: 'user:beth',
relation: 'reader',
object: 'repo:contoso/tooling',
}, {
authorization_model_id: '1uHxCSuTP0VKPYSnkq1pbb1jeZw',
});

// 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 Okta FGA authorization model:

model
schema 1.1

type team
relations
define member: [user, team#member]