> ## Documentation Index
> Fetch the complete documentation index at: https://openfga.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Modeling Authorization for Slack with OpenFGA

> Modeling authorization for Slack

export const WriteRequestViewer = ({relationshipTuples, deleteRelationshipTuples, authorizationModelId, skipSetup, conflictOptions, allowedLanguages}) => {
  const [runtime, setRuntime] = useState(null);
  const [loadError, setLoadError] = useState(null);
  const [selectedLanguage, setSelectedLanguage] = useState(null);
  useEffect(() => {
    if (window.openfgaViewer) {
      setLoadError(null);
      setRuntime(window.openfgaViewer);
      return;
    }
    let script = document.querySelector('script[src="/openfga-viewer.js"]');
    const loaded = () => {
      clearTimeout(timeout);
      if (window.openfgaViewer) {
        setLoadError(null);
        setRuntime(window.openfgaViewer);
      } else setLoadError('The OpenFGA example helper did not initialize. Reload this page to retry.');
    };
    const failed = () => {
      clearTimeout(timeout);
      setLoadError('Unable to load OpenFGA examples. Reload this page to retry.');
    };
    if (!script) {
      script = document.createElement('script');
      script.src = '/openfga-viewer.js';
    }
    script.addEventListener('load', loaded);
    script.addEventListener('error', failed);
    const timeout = setTimeout(() => {
      if (!window.openfgaViewer) failed();
    }, 10000);
    if (!script.isConnected) document.head.appendChild(script);
    return () => {
      clearTimeout(timeout);
      script.removeEventListener('load', loaded);
      script.removeEventListener('error', failed);
    };
  }, []);
  if (loadError) return <div className="openfga-code-viewer__status" role="alert">
        {loadError}
      </div>;
  if (!runtime) return <div className="openfga-code-viewer__status" role="status">
        Loading examples...
      </div>;
  const {languageLabels: LANG_LABEL, languageGrammars: LANG_CODE} = runtime;
  const langs = runtime.selectLanguages('WriteRequestViewer', allowedLanguages);
  const activeLang = langs.includes(selectedLanguage) ? selectedLanguage : langs[0];
  const setupLangs = langs.filter(runtime.hasSetup);
  const buildSetupCode = lang => runtime.buildSdkSetup(lang, 'WriteRequestViewer');
  const buildCode = lang => runtime.buildRequestCode(lang, 'WriteRequestViewer', {
    relationshipTuples,
    deleteRelationshipTuples,
    authorizationModelId,
    conflictOptions
  });
  return <div data-openfga-viewer="WriteRequestViewer">
      {!skipSetup && runtime.hasSetup(activeLang) && <Accordion title="Initialize the SDK">
          <p>
            Install the <a href="https://openfga.dev/docs/getting-started/install-sdk">SDK or CLI</a> and deploy an OpenFGA server. Set
            FGA_API_URL, FGA_STORE_ID, and optionally FGA_MODEL_ID. These examples use no authentication; see{' '}
            <a href="https://openfga.dev/docs/getting-started/setup-sdk-client">client setup</a> for authentication and runtime
            prerequisites. Go snippets run inside main; Python requests run inside an async function and the client must
            be closed afterward.
          </p>
          <CodeGroup key={setupLangs.join(',')} onChange={index => setSelectedLanguage(setupLangs[index])}>
            {setupLangs.map(lang => <code key={lang} className={`language-${LANG_CODE[lang]}`} language={LANG_CODE[lang]} filename={LANG_LABEL[lang]}>
                {buildSetupCode(lang)}
              </code>)}
          </CodeGroup>
        </Accordion>}
      <CodeGroup key={langs.join(',')} onChange={index => setSelectedLanguage(langs[index])}>
        {langs.map(lang => <code key={lang} className={`language-${LANG_CODE[lang]}`} language={LANG_CODE[lang]} filename={LANG_LABEL[lang]}>
            {buildCode(lang)}
          </code>)}
      </CodeGroup>
    </div>;
};

export const CheckRequestViewer = ({user, relation, object, allowed, contextualTuples, context, headers, authorizationModelId, skipSetup, allowedLanguages, consistency}) => {
  const [runtime, setRuntime] = useState(null);
  const [loadError, setLoadError] = useState(null);
  const [selectedLanguage, setSelectedLanguage] = useState(null);
  useEffect(() => {
    if (window.openfgaViewer) {
      setLoadError(null);
      setRuntime(window.openfgaViewer);
      return;
    }
    let script = document.querySelector('script[src="/openfga-viewer.js"]');
    const loaded = () => {
      clearTimeout(timeout);
      if (window.openfgaViewer) {
        setLoadError(null);
        setRuntime(window.openfgaViewer);
      } else setLoadError('The OpenFGA example helper did not initialize. Reload this page to retry.');
    };
    const failed = () => {
      clearTimeout(timeout);
      setLoadError('Unable to load OpenFGA examples. Reload this page to retry.');
    };
    if (!script) {
      script = document.createElement('script');
      script.src = '/openfga-viewer.js';
    }
    script.addEventListener('load', loaded);
    script.addEventListener('error', failed);
    const timeout = setTimeout(() => {
      if (!window.openfgaViewer) failed();
    }, 10000);
    if (!script.isConnected) document.head.appendChild(script);
    return () => {
      clearTimeout(timeout);
      script.removeEventListener('load', loaded);
      script.removeEventListener('error', failed);
    };
  }, []);
  if (loadError) return <div className="openfga-code-viewer__status" role="alert">
        {loadError}
      </div>;
  if (!runtime) return <div className="openfga-code-viewer__status" role="status">
        Loading examples...
      </div>;
  const {languageLabels: LANG_LABEL, languageGrammars: LANG_CODE} = runtime;
  const langs = runtime.selectLanguages('CheckRequestViewer', allowedLanguages);
  const activeLang = langs.includes(selectedLanguage) ? selectedLanguage : langs[0];
  const setupLangs = langs.filter(runtime.hasSetup);
  const buildSetupCode = lang => runtime.buildSdkSetup(lang, 'CheckRequestViewer');
  const buildCode = lang => runtime.buildRequestCode(lang, 'CheckRequestViewer', {
    user,
    relation,
    object,
    allowed,
    contextualTuples,
    context,
    headers,
    authorizationModelId,
    consistency
  });
  return <div data-openfga-viewer="CheckRequestViewer">
      {!skipSetup && runtime.hasSetup(activeLang) && <Accordion title="Initialize the SDK">
          <p>
            Install the <a href="https://openfga.dev/docs/getting-started/install-sdk">SDK or CLI</a> and deploy an OpenFGA server. Set
            FGA_API_URL, FGA_STORE_ID, and optionally FGA_MODEL_ID. These examples use no authentication; see{' '}
            <a href="https://openfga.dev/docs/getting-started/setup-sdk-client">client setup</a> for authentication and runtime
            prerequisites. Go snippets run inside main; Python requests run inside an async function and the client must
            be closed afterward.
          </p>
          <CodeGroup key={setupLangs.join(',')} onChange={index => setSelectedLanguage(setupLangs[index])}>
            {setupLangs.map(lang => <code key={lang} className={`language-${LANG_CODE[lang]}`} language={LANG_CODE[lang]} filename={LANG_LABEL[lang]}>
                {buildSetupCode(lang)}
              </code>)}
          </CodeGroup>
        </Accordion>}
      <CodeGroup key={langs.join(',')} onChange={index => setSelectedLanguage(langs[index])}>
        {langs.map(lang => <code key={lang} className={`language-${LANG_CODE[lang]}`} language={LANG_CODE[lang]} filename={LANG_LABEL[lang]}>
            {buildCode(lang)}
          </code>)}
      </CodeGroup>
    </div>;
};

export const AuthzModelSnippetViewer = ({configuration, syntaxesToShow = ['dsl', 'json'], skipVersion}) => {
  const [codegenReady, setCodegenReady] = useState(false);
  const [dslReady, setDslReady] = useState(false);
  const [activeTab, setActiveTab] = useState(syntaxesToShow[0] || 'dsl');
  useEffect(() => {
    if (window.fgaCodegen) {
      setCodegenReady(true);
      return;
    }
    let script = document.querySelector('script[src="/fga-codegen.js"]');
    const loaded = () => {
      if (window.fgaCodegen) setCodegenReady(true);
    };
    if (!script) {
      script = document.createElement('script');
      script.src = '/fga-codegen.js';
    }
    script.addEventListener('load', loaded);
    if (!script.isConnected) document.head.appendChild(script);
    return () => script.removeEventListener('load', loaded);
  }, []);
  useEffect(() => {
    if (window.openfgaDsl) {
      setDslReady(true);
      return;
    }
    let script = document.querySelector('script[src="/openfga-dsl-highlight.js"]');
    const loaded = () => {
      if (window.openfgaDsl) setDslReady(true);
    };
    if (!script) {
      script = document.createElement('script');
      script.src = '/openfga-dsl-highlight.js';
    }
    script.addEventListener('load', loaded);
    if (!script.isConnected) document.head.appendChild(script);
    return () => script.removeEventListener('load', loaded);
  }, []);
  if (!codegenReady) {
    return <div className="openfga-code-viewer__status" role="status">Loading model viewer...</div>;
  }
  const {transformer} = window.fgaCodegen;
  const getDsl = () => {
    try {
      const isTypeDefinition = !configuration.type_definitions && Boolean(configuration.type);
      const model = isTypeDefinition ? {
        schema_version: '1.1',
        type_definitions: [configuration]
      } : configuration;
      const dsl = transformer.transformJSONToDSL(model);
      return skipVersion || isTypeDefinition ? dsl.replace('model\n  schema 1.1\n', '') : dsl;
    } catch (e) {
      return `// error converting to DSL: ${e.message}`;
    }
  };
  const getJson = () => {
    const json = {
      ...configuration
    };
    delete json.id;
    if (skipVersion) delete json.schema_version;
    return JSON.stringify(json, null, 2);
  };
  const showTabs = syntaxesToShow.length > 1;
  const DSL_COLORS = dslReady && window.openfgaDsl ? window.openfgaDsl.colors : {};
  const THEME_STYLE = {
    '--openfga-dark-background': DSL_COLORS.background,
    '--openfga-dark-default': DSL_COLORS.default,
    '--openfga-dark-accent': DSL_COLORS.green
  };
  const renderDsl = () => {
    const code = getDsl().replace(/^\n/, '').replace(/\n$/, '');
    const hasError = code.startsWith('// error converting to DSL:');
    const tokens = hasError ? [{
      text: code,
      type: 'error'
    }] : dslReady && window.openfgaDsl ? window.openfgaDsl.tokenize(code) : [{
      text: code,
      type: 'default'
    }];
    return <div className="openfga-code-viewer__code" data-language="dsl" data-state={hasError ? 'error' : undefined} role={hasError ? 'alert' : undefined}>
        {tokens.map((t, i) => <span className="openfga-code-token" data-token={t.type || 'default'} key={i} style={{
      '--openfga-dark-token-color': t.color || DSL_COLORS.default
    }}>
            {t.text}
          </span>)}
      </div>;
  };
  const tokenizeJson = text => {
    const strRanges = [];
    const claims = [];
    let m;
    const strRe = /"(?:[^"\\]|\\.)*"/g;
    while (m = strRe.exec(text)) {
      const s = m.index, e = s + m[0].length;
      strRanges.push({
        s,
        e
      });
      const isKey = (/^\s*:/).test(text.slice(e));
      claims.push({
        s,
        e,
        type: isKey ? 'json-key' : 'json-string'
      });
    }
    const inStr = (s, e) => strRanges.some(r => s >= r.s && e <= r.e);
    const numRe = /-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
    while (m = numRe.exec(text)) {
      const s = m.index, e = s + m[0].length;
      if (!inStr(s, e)) claims.push({
        s,
        e,
        type: 'json-number'
      });
    }
    const kwRe = /\b(true|false|null)\b/g;
    while (m = kwRe.exec(text)) {
      const s = m.index, e = s + m[0].length;
      if (!inStr(s, e)) claims.push({
        s,
        e,
        type: 'json-keyword'
      });
    }
    const puncRe = /[{}\[\]:,]/g;
    while (m = puncRe.exec(text)) {
      const s = m.index;
      if (!inStr(s, s + 1)) claims.push({
        s,
        e: s + 1,
        type: 'json-punctuation'
      });
    }
    claims.sort((a, b) => a.s - b.s);
    const tokens = [];
    let cursor = 0;
    for (const c of claims) {
      if (c.s > cursor) tokens.push({
        text: text.slice(cursor, c.s),
        type: 'default'
      });
      tokens.push({
        text: text.slice(c.s, c.e),
        type: c.type
      });
      cursor = c.e;
    }
    if (cursor < text.length) tokens.push({
      text: text.slice(cursor),
      type: 'default'
    });
    return tokens;
  };
  const renderJson = () => {
    const code = getJson();
    const tokens = tokenizeJson(code);
    return <div className="openfga-code-viewer__code" data-language="json">
        {tokens.map((t, i) => <span className="openfga-code-token" data-token={t.type} key={i}>{t.text}</span>)}
      </div>;
  };
  return <div className="openfga-code-viewer" style={THEME_STYLE}>
      {showTabs && <div className="openfga-code-viewer__tabs">
          {syntaxesToShow.map(fmt => <button aria-pressed={activeTab === fmt} className="openfga-code-viewer__tab" data-state={activeTab === fmt ? 'active' : 'inactive'} key={fmt} onClick={() => setActiveTab(fmt)}>
              {fmt.toUpperCase()}
            </button>)}
        </div>}
      {(!showTabs || activeTab === 'dsl') && syntaxesToShow.includes('dsl') && renderDsl()}
      {(!showTabs || activeTab === 'json') && syntaxesToShow.includes('json') && renderJson()}
    </div>;
};

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

<Note>
  **What you will learn**

  * How to indicate relationships between a group of **[users](/docs/concepts#what-is-a-user)** and an **[object](/docs/concepts#what-is-an-object)**.
    Used here to indicate that all members of a slack workspace can write in a certain channel.
    See [Modeling User Groups](/docs/modeling/user-groups) for more.
  * How to Model **concentric relationship** to have a certain [relation](/docs/concepts#what-is-a-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](/docs/modeling/building-blocks/concentric-relationships) for more.
  * How to use [**the union operator**](/docs/configuration-language#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.
</Note>

## Before you start

In order to understand this guide correctly you must be familiar with some OpenFGA concepts and know how to develop the things that we will list below.

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

<Accordion title="OpenFGA concepts" id="-concepts">
  <h4 id="direct-access">Direct access</h4>

  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 →](/docs/modeling/direct-access)

  <h4 id="modeling-concentric-relationships">Modeling concentric relationships</h4>

  You need to know how to update the authorization model to allow having nested relations such as all writers are readers. [Learn more →](/docs/modeling/building-blocks/concentric-relationships)

  <h4 id="concepts--configuration-language">Concepts & configuration language</h4>

  * Some [OpenFGA Concepts](/docs/concepts)
  * [Configuration Language](/docs/configuration-language)
</Accordion>

## 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?](https://slack.com/intl/en-ca/help/articles/115004071768-What-is-Slack-))

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

> As reference, you can refer to Slack's publicly available docs:
>
> * [Role Management at Slack](https://slack.engineering/role-management-at-slack/)
> * [Types of Roles in Slack](https://slack.com/intl/en-ca/help/articles/360018112273-Types-of-roles-in-Slack)
> * [Permissions by Role in Slack](https://slack.com/intl/en-ca/help/articles/201314026-Permissions-by-role-in-Slack)
> * [Manage a Workspace on Enterprise Grid](https://slack.com/intl/en-ca/help/articles/115005225987-Manage-a-workspace-on-Enterprise-Grid)
> * [Manage channel posting permissions](https://slack.com/intl/en-ca/help/articles/360004635551-Manage-channel-posting-permissions-)

> 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**](https://slack.com/intl/en-ca/help/articles/360004635551-Manage-channel-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](https://slack.engineering/role-management-at-slack/))
* 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

<img src="https://mintcdn.com/fga/sFng5HcsvGPlDD3_/modeling/advanced/assets/slack-01.svg?fit=max&auto=format&n=sFng5HcsvGPlDD3_&q=85&s=dc0fcd7ed9b93bd079564eb39e9b6104" alt="Image showing requirements" width="1205" height="461" data-path="modeling/advanced/assets/slack-01.svg" />

<Warning>
  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.
</Warning>

<h2 id="modeling-workspaces--channels">Modeling workspaces & channels</h2>

The goal by the end of this post is to ask OpenFGA: 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 OpenFGA is based on [Zanzibar](https://zanzibar.academy), a Relation Based Access Control system. This means it relies on [objects](/docs/concepts#what-is-an-object) and [user](/docs/concepts#what-is-a-user) [relations](/docs/concepts#what-is-a-relation) to perform authorization [checks](/docs/concepts#what-is-a-check-request).

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 OpenFGA.

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 OpenFGA's [authorization model](/docs/concepts#what-is-an-authorization-model): You have a [type](/docs/concepts#what-is-a-type) called "workspace", and users can be related to it as a legacy\_admin, channels\_admin, member and guest

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
{
  type: 'user',
},
{
  type: 'workspace', // objects of type workspace
  relations: {
    // have users related to them as...
    legacy_admin: {
      // Legacy Admins
      this: {},
    },
    channels_admin: {
      // Channels Admin
      this: {},
    },
    member: {
      // Member
      this: {},
    },
    guest: {
      // Guest
      this: {},
    },
  },
  metadata: {
    relations: {
      legacy_admin: { directly_related_user_types: [{ type: 'user' }] },
      channels_admin: { directly_related_user_types: [{ type: 'user' }] },
      member: { directly_related_user_types: [{ type: 'user' }] },
      guest: { directly_related_user_types: [{ type: 'user' }] },
    },
  },
}]
}}
/>

<Note>
  **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](/docs/configuration-language#direct-relationship-type-restrictions) indicate that a user can have a [direct relationship](/docs/concepts#what-are-direct-and-implied-relationships) with an object of the type the relation specifies.
</Note>

### 01. Individual permissions

To keep things simple and focus on OpenFGA 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

| User | Relation | Object |
| - | - | - |
| amy | legacy\_admin | workspace:sandcastle |
| bob | channels\_admin | workspace:sandcastle |
| catherine | member | workspace:sandcastle |
| david | guest | workspace:sandcastle |
| emily | member | workspace:sandcastle |

To represent permissions in OpenFGA we use [relations](/docs/concepts#what-is-a-relation). For workspace permissions we need to create the following [authorization model](/docs/concepts#what-is-an-authorization-model):

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'workspace',
    relations: {
      legacy_admin: {
        this: {},
      },
      channels_admin: {
        this: {},
      },
      member: {
        this: {},
      },
      guest: {
        this: {},
      },
    },
    metadata: {
      relations: {
        legacy_admin: { directly_related_user_types: [{ type: 'user' }] },
        channels_admin: { directly_related_user_types: [{ type: 'user' }] },
        member: { directly_related_user_types: [{ type: 'user' }] },
        guest: { directly_related_user_types: [{ type: 'user' }] },
      },
    },
  },
],
}}
/>

The OpenFGA service determines if a [user](/docs/concepts#what-is-a-user) has access to an [object](/docs/concepts#what-is-an-object) by [checking](/docs/concepts#what-is-a-check-request) if the user has a relation to that object. Let us examine one of those relations in detail:

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type: 'workspace', // objects of type workspace
relations: {
  // have users related to them as...
  member: {
    // "member": if those users belong to:
    this: {}, // the userset of all users related to the document as "member"
  },
},
metadata: {
  relations: {
    member: { directly_related_user_types: [{ type: 'user' }] },
  },
},
}}
  skipVersion={true}
/>

<Note>
  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](/docs/concepts#what-are-direct-and-implied-relationships) as a member to an object of type "workspace"
</Note>

If we want to say `amy` is a `legacy_admin` of `workspace:sandcastle` we create this relationship tuple

<WriteRequestViewer
  relationshipTuples={[
{
  _description: 'Amy is a Legacy Admin in the Sandcastle workspace',
  user: 'user:amy',
  relation: 'legacy_admin',
  object: 'workspace:sandcastle',
},
]}
/>

We can now ask OpenFGA "is `amy` a legacy\_admin of **workspace:sandcastle**?"

<CheckRequestViewer user={'user:amy'} relation={'legacy_admin'} object={'workspace:sandcastle'} allowed={true} />

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

<WriteRequestViewer
  relationshipTuples={[
{
  _description: 'Catherine is a Member in the Sandcastle workspace',
  user: 'user:catherine',
  relation: 'member',
  object: 'workspace:sandcastle',
},
]}
/>

And verify by [asking](/docs/concepts#what-is-a-check-request) OpenFGA

<CheckRequestViewer user={'user:catherine'} relation={'member'} object={'workspace:sandcastle'} allowed={true} />

Catherine, on the other hand, is not a legacy\_admin of workspace:sandcastle.

<CheckRequestViewer user={'user:catherine'} relation={'legacy_admin'} object={'workspace:sandcastle'} allowed={false} />

Repeat this process for the other relationships

<WriteRequestViewer
  relationshipTuples={[
{
  _description: 'Bob is a Channels Admin in the Sandcastle workspace',
  user: 'user:bob',
  relation: 'channels_admin',
  object: 'workspace:sandcastle',
},
{
  _description: 'David is a guest in the Sandcastle workspace',
  user: 'user:david',
  relation: 'guest',
  object: 'workspace:sandcastle',
},
{
  _description: 'Emily is a Member in the Sandcastle workspace',
  user: 'user:emily',
  relation: 'member',
  object: 'workspace:sandcastle',
},
]}
/>

#### Verification

To verify, we can issue [check request](/docs/concepts#what-is-a-check-request) to verify it is working as expected.

<CheckRequestViewer user={'user:amy'} relation={'legacy_admin'} object={'workspace:sandcastle'} allowed={true} />

Let's try to verify the followings:

| User | Object | Relation | Query | Relation? |
| - | - | - | - | - |
| `amy` | `workspace:sandcastle` | `legacy_admin` | `is amy related to workspace:sandcastle as legacy_admin?` | Yes |
| `david` | `workspace:sandcastle` | `legacy_admin` | `is david related to workspace:sandcastle as legacy_admin?` | No |
| `amy` | `workspace:sandcastle` | `guest` | `is amy related to workspace:sandcastle as guest?` | No |
| `david` | `workspace:sandcastle` | `guest` | `is david related to workspace:sandcastle as guest?` | Yes |
| `amy` | `workspace:sandcastle` | `member` | `is amy related to workspace:sandcastle as member?` | No |
| `david` | `workspace:sandcastle` | `member` | `is david related to workspace:sandcastle as member?` | No |

### 02. Updating The `workspace` Authorization Model With Implied Relations

Some of the queries that you ran earlier, while returning the correct response, do not match reality. One of which is:

<CheckRequestViewer user={'user:amy'} relation={'member'} object={'workspace:sandcastle'} allowed={false} />

As you saw before, running this query will return `amy is not a member of workspace:sandcastle`, which is correct based on the data you have given OpenFGA so far. But in reality, Amy, who is a `legacy_admin` already has an [implied](/docs/concepts#what-are-direct-and-implied-relationships) `channels_admin` and `member` relations. In fact anyone (other than a guest) is a `member` of the workspace.

To change this behavior, we will update our system with a [**concentric relationship**](/docs/modeling/building-blocks/concentric-relationships) model.

With the following updated [authorization model](/docs/concepts#what-is-a-type-definition), you are informing OpenFGA that any user who is related to a workspace as `legacy_admin`, is also related as a `channels_admin` and a `member` .

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'workspace',
    relations: {
      legacy_admin: {
        this: {},
      },
      channels_admin: {
        // users related to `workspace` as `channels_admin` are
        union: {
          // the union (any of):
          child: [
            {
              this: {}, // the set of users with a direct `channels_admin` relation
            },
            {
              computedUserset: {
                // the set of users related to the workspace as `legacy_admin`
                relation: 'legacy_admin',
              },
            },
          ],
        },
      },
      member: {
        // users related to `workspace` as `member` are
        union: {
          // the union (any of):
          child: [
            {
              this: {}, // the set of users with a direct `member` relation
            },
            {
              computedUserset: {
                // the set of users related to the workspace as `channels_admin`
                relation: 'channels_admin',
              },
            },
            {
              computedUserset: {
                // the set of users related to the workspace as `legacy_admin`
                relation: 'legacy_admin',
              },
            },
          ],
        },
      },
      guest: {
        this: {},
      },
    },
    metadata: {
      relations: {
        legacy_admin: { directly_related_user_types: [{ type: 'user' }] },
        channels_admin: { directly_related_user_types: [{ type: 'user' }] },
        member: { directly_related_user_types: [{ type: 'user' }] },
        guest: { directly_related_user_types: [{ type: 'user' }] },
      },
    },
  },
],
}}
/>

We can then verify `amy` is a `member` of `workspace:sandcastle`.

<CheckRequestViewer user={'user:amy'} relation={'member'} object={'workspace:sandcastle'} allowed={true} />

We can check for other users and relationships.

| User | Object | Relation | Query | Relation? |
| - | - | - | - | - |
| `amy` | `workspace:sandcastle` | `legacy_admin` | `is amy related to workspace:sandcastle as legacy_admin?` | Yes |
| `david` | `workspace:sandcastle` | `legacy_admin` | `is david related to workspace:sandcastle as legacy_admin?` | No |
| `amy` | `workspace:sandcastle` | `guest` | `is amy related to workspace:sandcastle as guest?` | No |
| `david` | `workspace:sandcastle` | `guest` | `is david related to workspace:sandcastle as guest?` | Yes |
| `amy` | `workspace:sandcastle` | `member` | `is amy related to workspace:sandcastle as member?` | Yes |
| `david` | `workspace:sandcastle` | `member` | `is david related to workspace:sandcastle as member?` | No |

### 03. Updating the authorization model to include channels

So far, you have modeled the users' [relations](/docs/concepts#what-is-a-relation) to the workspace itself. In this task you will expand the model to include the relations concerning the channels.

By the end of it, you will run some queries to check whether a user can view or write to a certain channel. Queries such as:

* `is david related to channel:general as viewer?` (expected answer: No relation, as David is a guest user with only a relation to #proj-marketing-campaign)
* `is david related to channel:proj_marketing_campaign as viewer?` (expected answer: There is a relation, as there is a relation between David and #proj-marketing-campaign as a writer)
* `is bob related to channel:general as viewer?` (expected answer: There is a relation, as Bob is a member of the Sandcastle workspace, and all members of the workspace have a viewer relation to #general)

The requirements are:

* **Amy**, **Bob**, **Catherine** and **Emily**, are normal members of the Sandcastle workspace, they can **view** all **public channels**, in this case: #general and #proj-marketing-campaign
* **David**, a guest user, has only **view** and **write** access to the **#proj-marketing-campaign channel**
* **Bob** and **Emily** are the only ones with either **view** or **write** access to the **#marketing-internal channel**
* **Amy** and **Emily** are the only ones with **write** access to the **#general channel**

The possible relations to channels are:

* Workspace includes the channel, consider the relation that of a **parent workspace**
* A user can be a **viewer** and/or **writer** on a channel

The authorization model already has a section describing the workspace, what remains is describing the channel. That can be done by adding the following section to the configuration above:

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type: 'channel', // A channel can have the following relations:
relations: {
  parent_workspace: {
    // workspaces related to it as `parent_workspace`
    this: {},
  },
  writer: {
    // users related to it as `writer`
    this: {},
  },
  viewer: {
    // users related to it as `viewer`
    this: {},
  },
},
metadata: {
  relations: {
    parent_workspace: { directly_related_user_types: [{ type: 'workspace' }] },
    writer: {
      directly_related_user_types: [
        { type: 'user' },
        { type: 'workspace', relation: 'legacy_admin' },
        { type: 'workspace', relation: 'channels_admin' },
        { type: 'workspace', relation: 'member' },
        { type: 'workspace', relation: 'guest' },
      ],
    },
    viewer: {
      directly_related_user_types: [
        { type: 'user' },
        { type: 'workspace', relation: 'legacy_admin' },
        { type: 'workspace', relation: 'channels_admin' },
        { type: 'workspace', relation: 'member' },
        { type: 'workspace', relation: 'guest' },
      ],
    },
  },
},
}}
  skipVersion={true}
/>

<Note>
  The configuration snippet above describes a channel that can have the following relations:

  * workspaces related to it as `parent_workspace`
  * users related to it as `writer`
  * users related to it as `viewer`
</Note>

#### Implied relation

There is an [implied relation](/docs/concepts#what-are-direct-and-implied-relationships) that anyone who can write to a channel can also read from it, so the authorization model can be modified to be:

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type: 'channel',
relations: {
  parent_workspace: {
    this: {},
  },
  writer: {
    this: {},
  },
  viewer: {
    // viewer is the union of the set of users with a direct viewer relation, and the set of users with writer relations
    union: {
      child: [
        {
          // a user can be assigned a direct `viewer` relation, i.e., not implied through another relation
          this: {},
        },
        {
          // a user that is a writer is also implicitly a viewer
          computedUserset: {
            relation: 'writer',
          },
        },
      ],
    },
  },
},
metadata: {
  relations: {
    parent_workspace: { directly_related_user_types: [{ type: 'workspace' }] },
    writer: {
      directly_related_user_types: [
        { type: 'user' },
        { type: 'workspace', relation: 'legacy_admin' },
        { type: 'workspace', relation: 'channels_admin' },
        { type: 'workspace', relation: 'member' },
        { type: 'workspace', relation: 'guest' },
      ],
    },
    viewer: {
      directly_related_user_types: [
        { type: 'user' },
        { type: 'workspace', relation: 'legacy_admin' },
        { type: 'workspace', relation: 'channels_admin' },
        { type: 'workspace', relation: 'member' },
        { type: 'workspace', relation: 'guest' },
      ],
    },
  },
},
}}
  skipVersion={true}
/>

<Note>
  Note that the channel type definition has been updated to indicate that viewer is the union of:

  * the set of users with a [direct](/docs/concepts#what-are-direct-and-implied-relationships) viewer relation to this object
  * the set of users with writer relations to this object
</Note>

As a result, the authorization model is:

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'workspace',
    relations: {
      legacy_admin: {
        this: {},
      },
      channels_admin: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'legacy_admin',
              },
            },
          ],
        },
      },
      member: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'channels_admin',
              },
            },
            {
              computedUserset: {
                relation: 'legacy_admin',
              },
            },
          ],
        },
      },
      guest: {
        this: {},
      },
    },
    metadata: {
      relations: {
        legacy_admin: { directly_related_user_types: [{ type: 'user' }] },
        channels_admin: { directly_related_user_types: [{ type: 'user' }] },
        member: { directly_related_user_types: [{ type: 'user' }] },
        guest: { directly_related_user_types: [{ type: 'user' }] },
      },
    },
  },
  {
    type: 'channel',
    relations: {
      parent_workspace: {
        this: {},
      },
      writer: {
        this: {},
      },
      viewer: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'writer',
              },
            },
          ],
        },
      },
    },
    metadata: {
      relations: {
        parent_workspace: { directly_related_user_types: [{ type: 'workspace' }] },
        writer: {
          directly_related_user_types: [
            { type: 'user' },
            { type: 'workspace', relation: 'legacy_admin' },
            { type: 'workspace', relation: 'channels_admin' },
            { type: 'workspace', relation: 'member' },
            { type: 'workspace', relation: 'guest' },
          ],
        },
        viewer: {
          directly_related_user_types: [
            { type: 'user' },
            { type: 'workspace', relation: 'legacy_admin' },
            { type: 'workspace', relation: 'channels_admin' },
            { type: 'workspace', relation: 'member' },
            { type: 'workspace', relation: 'guest' },
          ],
        },
      },
    },
  },
],
}}
/>

#### Updating relationship tuples

What remains is to add the [relationship tuples](/docs/concepts#what-is-a-relationship-tuple) to indicate the relation between the users, workspace and the channels.

The Sandcastle workspace is a parent workspace of the #general, #marketing-internal and #proj-marketing-campaign channels.

<WriteRequestViewer
  relationshipTuples={[
{
  user: 'workspace:sandcastle',
  relation: 'parent_workspace',
  object: 'channel:general',
},
{
  user: 'workspace:sandcastle',
  relation: 'parent_workspace',
  object: 'channel:marketing_internal',
},
{
  user: 'workspace:sandcastle',
  relation: 'parent_workspace',
  object: 'channel:proj_marketing_campaign',
},
]}
/>

##### <span id="general-channel" style={{ scrollMarginTop: '7rem' }}><code>#general</code> channel</span>

The `#general` channel is a public channel visible to all the members of the workspace. In OpenFGA, you represent this relation in the form of the following relationship tuple:

<WriteRequestViewer
  relationshipTuples={[
{
  _description:
    'The set of users related to `workspace:sandcastle` as member are also related to `channel:general` as `viewer`',
  user: 'workspace:sandcastle#member',
  relation: 'viewer',
  object: 'channel:general',
},
]}
/>

<Note>
  This indicates The set of users related to `workspace:sandcastle` as member are also related to `channel:general` as `viewer`
</Note>

And to indicate that Amy and Emily can write to it:

<WriteRequestViewer
  relationshipTuples={[
{
  _description:
    'Due to the configuration update you added earlier, writer relation is enough to imply a viewer relation',
  user: 'user:amy',
  relation: 'writer',
  object: 'channel:general',
},
{
  user: 'user:emily',
  relation: 'writer',
  object: 'channel:general',
},
]}
/>

##### <span id="marketing-internal-channel" style={{ scrollMarginTop: '7rem' }}><code>#marketing-internal</code> channel</span>

The `#marketing-internal` is visible to only Bob and Emily. They can view and write in it.

<WriteRequestViewer
  relationshipTuples={[
{
  user: 'user:bob',
  relation: 'writer',
  object: 'channel:marketing_internal',
},
{
  user: 'user:emily',
  relation: 'writer',
  object: 'channel:marketing_internal',
},
]}
/>

##### <span id="proj-marketing-campaign-channel" style={{ scrollMarginTop: '7rem' }}><code>#proj-marketing-campaign</code> channel</span>

The `#proj-marketing-campaign` is public to all members of the Sandcastle workspace. They can view and write in it.

<WriteRequestViewer
  relationshipTuples={[
{
  user: 'workspace:sandcastle#member',
  relation: 'writer',
  object: 'channel:proj_marketing_campaign',
},
]}
/>

David is a guest user who can also view and write to #proj-marketing-campaign

<WriteRequestViewer
  relationshipTuples={[
{
  user: 'user:david',
  relation: 'writer',
  object: 'channel:proj_marketing_campaign',
},
]}
/>

<h4 id="verification-1">Verification</h4>

Now that you have added the necessary relationship tuples, you will check to make sure that your configuration is valid.

First, we want to ensure david is not related to channel:general as viewer.

<CheckRequestViewer user={'user:david'} relation={'viewer'} object={'channel:general'} allowed={false} />

David should be related to channel:proj\_marketing\_campaign as viewer.

<CheckRequestViewer user={'user:david'} relation={'viewer'} object={'channel:proj_marketing_campaign'} allowed={true} />

Repeat this for the following relations

| User | Object | Relation | Query | Relation? |
| - | - | - | - | - |
| `amy` | `workspace:sandcastle` | `legacy_admin` | `is amy related to workspace:sandcastle as legacy_admin?` | Yes |
| `amy` | `workspace:sandcastle` | `member` | `is amy related to workspace:sandcastle as member?` | Yes |
| `amy` | `workspace:sandcastle` | `channels_admin` | `is amy related to workspace:sandcastle as channels_admin?` | Yes |
| `amy` | `channel:general` | `writer` | `is amy related to channel:general as writer?` | Yes |
| `amy` | `channel:general` | `viewer` | `is amy related to channel:general as viewer?` | Yes |
| `amy` | `channel:marketing_internal` | `writer` | `is amy related to channel:marketing_internal as writer?` | No |
| `amy` | `channel:marketing_internal` | `viewer` | `is amy related to channel:marketing_internal as viewer?` | No |
| `emily` | `channel:marketing_internal` | `writer` | `is emily related to channel:marketing_internal as writer?` | Yes |
| `emily` | `channel:marketing_internal` | `viewer` | `is emily related to channel:marketing_internal as viewer?` | Yes |
| `david` | `workspace:sandcastle` | `guest` | `is david related to workspace:sandcastle as guest?` | Yes |
| `david` | `workspace:sandcastle` | `member` | `is david related to workspace:sandcastle as member?` | No |
| `david` | `channel:general` | `viewer` | `is david related to channel:general as viewer?` | No |
| `david` | `channel:marketing_internal` | `viewer` | `is david related to channel:marketing_internal as viewer?` | No |
| `david` | `channel:proj_marketing_campaign` | `viewer` | `is david related to channel:proj_marketing_campaign as viewer?` | Yes |

## Summary

* Have a basic understanding of [authorization](/docs/authorization-concepts#authentication-and-authorization) and [OpenFGA Concepts](/docs/concepts).
* Understand how to model authorization for a communication platform like Slack using OpenFGA.

In this tutorial, you:

* were introduced to [fine grain authentication](/docs/authorization-concepts#what-is-fine-grained-authorization) and OpenFGA.
* learned how to build and test an OpenFGA authorization model for a communication platforms like Slack.

Upcoming tutorials will dive deeper into OpenFGA, 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.

If you are interested in learning more about Authorization and Role Management at Slack, check out the Auth0 Fine-Grained Authorization (FGA) team's chat with the Slack engineering team.

<iframe style={{ marginTop: 36, borderRadius: 8 }} width="100%" height="500" src="https://www.youtube-nocookie.com/embed/-iVBsagaK5Y" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowFullScreen />

### Exercises for you

* Try adding more relationship tuples to represent other users and channels being added. Then run queries to make sure that the authorization model remains valid.
* Update the configuration to model more Slack permissions (workspace owners, Slack orgs), then add the relationship tuples necessary and run some queries to validate your configuration.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.