Skip to main content

Modeling Authorization for Slack with Okta FGA

This tutorial explains how to model permissions for a communication platform like Slack using Okta FGA.

What you will learn
  • How to indicate relationships between a group of users and an object.
    Used here to indicate that all members of a slack workspace can write in a certain channel.
    See Modeling User Groups for more.
  • How to Model concentric relationship to have a certain relation on an object imply another relation on the same object.
    Used here to indicate that legacy admins have all the permissions of the more granular channels admin.
    See Modeling Concentric Relationships for more.
  • How to use 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.

Slack

Explore the Slack 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.

Direct Access

You need to know how to create an authorization model and create a relationship tuple to grant a user access to an object. Learn more →

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 →

Concepts & Configuration Language

What you will be modeling

Slack is a messaging app for businesses that connects people to the information they need. By bringing people together to work as one unified team, Slack transforms the way organizations communicate. (Source: What is Slack?)

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

As reference, you can refer to Slack's publicly available docs:

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

Requirements

This tutorial will focus on the following sections (this is a partial list of Slack's roles):

Workspace Roles:

  • Guest: This type of user is limited in their ability to use Slack, and is only permitted to see one or multiple delegated channels.
  • Member: This is the base type of user that does not have any particular administrative abilities, but has basic access to the organization's Slack workspaces. When an administrative change needs to be made, these users need the support of admins and owners to make the changes.
  • Legacy Admin: This type of user is the basic administrator of any organization, and can make a wide variety of administrative changes across Slack, such as renaming channels, archiving channels, setting up preferences and policies, inviting new users, and installing applications. Users with this role perform the majority of administrative tasks across a team.

System Roles:

  • Channels Admin: This type of user has the permission to archive channels, rename channels, create private channels, and convert public channels into private channels.

Channel Settings:

  • Visibility:
    • Public: Visible to all members and open to join
    • Private: Visible to admins and invited members
  • Posting Permissions:
    • Open: Anyone can post
    • Limited: Only allowed members can post

Defined Scenarios

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

There will be the following users:

  • Amy
  • Bob
  • Catherine
  • David
  • Emily

These users will interact in the following scenarios:

  • You will assume there is a Slack workspace called Sandcastle
  • Amy is a legacy admin of the Sandcastle workspace
  • Bob is a member of the Sandcastle workspace with a channels admin role (Read more about system roles at Slack here)
  • Catherine and Emily are normal members of the Sandcastle workspace, they can view all public channels, as well as channels they have been invited to
  • David is a guest user with only view and write access to #proj-marketing-campaign, one of the public channels in the Sandcastle workspace
  • Bob and Emily are in a private channel #marketing-internal in the Sandcastle workspace which only they can view and post to
  • All members of the Sandcastle workspace can view the general channel, but only Amy and Emily can post to it

Image showing requirements

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.

Modeling Workspaces & Channels

The goal by the end of this post is to ask Okta Fine Grained Authorization (FGA): Does person X have permission to perform action Y on channel Z? In response, you want to either get a confirmation that person X can indeed do that, or a rejection that they cannot. E.g. does David have access to view #general?

The Okta Fine Grained Authorization (FGA) is based on Zanzibar, a Relation Based Access Control system. This means it relies on objects and user relations to perform authorization checks.

Setting aside the permissions, you will start with the roles and learn how to express the requirements in terms of relations you can feed into Okta FGA.

The requirements stated:

  • Amy is a legacy admin of the Sandcastle workspace
  • Bob is a channels admin of the Sandcastle workspace
  • Catherine and Emily are a normal members of the Sandcastle workspace
  • David is a guest user

Here is how you would express than in Okta FGA's authorization model: You have a type called "workspace", and users can be related to it as a legacy_admin, channels_admin, member and guest

model
schema 1.1

type user

type workspace
relations
define legacy_admin: [user]
define channels_admin: [user]
define member: [user]
define guest: [user]
info

Objects of type workspace have users related to them as:

  • Legacy Admin (legacy_admin)
  • Channels Admin (channels_admin)
  • Member (member)
  • Guest (guest)

Direct relationship type restrictions indicate that a user can have a direct relationship with an object of the type the relation specifies.

01. Individual Permissions

To keep things simple and focus on Okta FGA rather than Slack complexity, we will model only four roles (legacy_admin, channels_admin, member, guest).

At the end of this section we want to have the following permissions represented

UserRelationObject
amylegacy_adminworkspace:sandcastle
bobchannels_adminworkspace:sandcastle
catherinememberworkspace:sandcastle
davidguestworkspace:sandcastle
emilymemberworkspace:sandcastle

To represent permissions in Okta FGA we use relations. For workspace permissions we need to create the following authorization model:

model
schema 1.1

type user

type workspace
relations
define legacy_admin: [user]
define channels_admin: [user]
define member: [user]
define guest: [user]

The Okta Fine Grained Authorization (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 workspace
relations
define member: [user]
info

The snippet above indicates that objects of type workspace have users related to them as "member" if those users belong to the userset of all users related to the workspace as "member".

This means that a user can be directly related as a member to an object of type "workspace"

If we want to say amy is a legacy_admin of workspace:sandcastle 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: [
// Amy is a Legacy Admin in the Sandcastle workspace
{"_description":"Amy is a Legacy Admin in the Sandcastle workspace","user":"user:amy","relation":"legacy_admin","object":"workspace:sandcastle"}
],
}, {
authorization_model_id: "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
});

We can now ask Okta FGA "is amy a legacy_admin of workspace:sandcastle?"

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:amy',
relation: 'legacy_admin',
object: 'workspace:sandcastle',
}, {
authorization_model_id: '1uHxCSuTP0VKPYSnkq1pbb1jeZw',
});

// allowed = true

We can also say that catherine is a member of workspace:sandcastle:

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: [
// Catherine is a Member in the Sandcastle workspace
{"_description":"Catherine is a Member in the Sandcastle workspace","user":"user:catherine","relation":"member","object":"workspace:sandcastle"}
],
}, {
authorization_model_id: "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
});

And verify by asking 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:catherine',
relation: 'member',
object: 'workspace:sandcastle',
}, {
authorization_model_id: '1uHxCSuTP0VKPYSnkq1pbb1jeZw',
});

// allowed = true

Catherine, on the other hand, is not a legacy_admin of workspace:sandcastle.

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:catherine',
relation: 'legacy_admin',
object: 'workspace:sandcastle',
}, {
authorization_model_id: '1uHxCSuTP0VKPYSnkq1pbb1jeZw',
});

// allowed = false

Repeat this process for the other relationships

[
{
// Bob is a Channels Admin in the Sandcastle workspace
user: 'user:bob',
relation: 'channels_admin',
object: 'workspace:sandcastle',
},
{
// David is a guest in the Sandcastle workspace
user: 'user:david',
relation: 'guest',
object: 'workspace:sandcastle',
},
{
// Emily is a Member in the Sandcastle workspace
user: 'user:emily',
relation: 'member',
object: 'workspace:sandcastle',
},
]
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: [
// Bob is a Channels Admin in the Sandcastle workspace
{"_description":"Bob is a Channels Admin in the Sandcastle workspace","user":"user:bob","relation":"channels_admin","object":"workspace:sandcastle"},
// David is a guest in the Sandcastle workspace
{"_description":"David is a guest in the Sandcastle workspace","user":"user:david","relation":"guest","object":"workspace:sandcastle"},
// Emily is a Member in the Sandcastle workspace
{"_description":"Emily is a Member in the Sandcastle workspace","user":"user:emily","relation":"member","object":"workspace:sandcastle"}
],
}, {
authorization_model_id: "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
});

Verification

To verify, we can issue check request to verify it is working as expected.

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:amy',
relation: 'legacy_admin',
object: 'workspace:sandcastle',
}, {
authorization_model_id: '1uHxCSuTP0VKPYSnkq1pbb1jeZw',
});

// allowed = true