Skip to main content

Modeling Entitlements for a System with Okta FGA

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

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

Entitlements

Explore the Entitlements 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 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 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 Okta 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 Okta 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 simple authorization 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
model
schema 1.1

type user

type feature
relations
define associated_plan: [plan]

type plan
relations
define subscriber: [organization]

type organization
relations
define member: [user]

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
// 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: [
// the free plan is the associated plan of the issues feature
{"_description":"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
{"_description":"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
{"_description":"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
{"_description":"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
{"_description":"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
{"_description":"the enterprise plan is the associated plan of the SAML Single Sign-on feature","user":"plan:enterprise","relation":"associated_plan","object":"feature:sso"}
],
}, {
authorization_model_id: "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
});

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

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: [
// the Alpha Beta Gamma organization is a subscriber of the free plan
{"_description":"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
{"_description":"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
{"_description":"the Cups and Dishes organization is a subscriber of the enterprise plan","user":"organization:cups","relation":"subscriber","object":"plan:enterprise"}
],
}, {
authorization_model_id: "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
});

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

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: [
// anne is a member of the Alpha Beta Gamma organization
{"_description":"anne is a member of the Alpha Beta Gamma organization","user":"user:anne","relation":"member","object":"organization:alpha"},
// beth is a member of the Bayer Water Supplies
{"_description":"beth is a member of the Bayer Water Supplies","user":"user:beth","relation":"member","object":"organization:bayer"},
// charles is a member of the Cups and Dishes organization
{"_description":"charles is a member of the Cups and Dishes organization","user":"user:charles","relation":"member","object":"organization:cups"}
],
}, {
authorization_model_id: "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
});

So far you have given Okta 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: 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 Okta FGA can detect a relation in this case.

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: 'member',
object: 'organization:alpha',
}, {
authorization_model_id: '1uHxCSuTP0VKPYSnkq1pbb1jeZw',
});

// allowed = true

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

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: 'member',
object: 'organization:bayer',
}, {
authorization_model_id: '1uHxCSuTP0VKPYSnkq1pbb1jeZw',
});

// allowed = false
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: 'organization:bayer',
relation: 'subscriber',
object: 'plan:team',
}, {
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: 'plan:free',
relation: 'associated_plan',
object: 'feature:issues',
}, {
authorization_model_id: '1uHxCSuTP0VKPYSnkq1pbb1jeZw',
});

// allowed = true