> ## 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 GitHub permissions with OpenFGA

> Modeling GitHub permissions

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 GitHub's Organization permission model using OpenFGA. [This article](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-organizations-and-teams/managing-access-to-your-organizations-repositories) from the GitHub docs has links to all other articles we are going to be exploring in this document.

<Note>
  **What you will learn**

  * Indicate [relationships](/docs/concepts#what-is-a-relationship) between a group of **[users](/docs/concepts#what-is-a-user)** and an **[object](/docs/concepts#what-is-an-object)**. See [Modeling User Groups](/docs/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](/docs/concepts#what-is-a-relation) on an object imply another relation on the same object. See [Modeling Concepts: Concentric Relationships](/docs/modeling/building-blocks/concentric-relationships) for more.
    Used here to indicate that maintainers of a repository are also writers of that repository.
  * Using [**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.
    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**](/docs/modeling/parent-child) to indicate that a user having a relationship with a certain object implies having a relationship with another object in OpenFGA.
    Used here to indicate that a repository admin on a GitHub organization, is an admin on all repositories that organization owns.
</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="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="modeling-object-to-object-relationships">Modeling object-to-object relationships</h4>

  You need to know how to create relationships between objects and how that might affect a user's relationships to those objects. [Learn more →](/docs/modeling/building-blocks/object-to-object-relationships)

  Used here to indicate that users who have repo admin access on an organization, have admin access to all repositories owned by that organization.

  <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

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 OpenFGA, 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](https://docs.github.com/en/organizations/managing-access-to-your-organizations-repositories/repository-roles-for-an-organization#repository-roles-for-organizations).

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

<h2 id="modeling-githubs-permissions">Modeling GitHub's permissions</h2>

### 01. Permissions For Individuals In An Org

GitHub has [5 different permission levels for repositories](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-organizations-and-teams/repository-permission-levels-for-an-organization):

<img src="https://mintcdn.com/fga/sFng5HcsvGPlDD3_/modeling/advanced/assets/github-permission-level.svg?fit=max&auto=format&n=sFng5HcsvGPlDD3_&q=85&s=a5a5ad63e14a854800ea35df8053797f" alt="Image showing github permission levels" width="1526" height="886" data-path="modeling/advanced/assets/github-permission-level.svg" />

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

<img src="https://mintcdn.com/fga/sFng5HcsvGPlDD3_/modeling/advanced/assets/github-01.svg?fit=max&auto=format&n=sFng5HcsvGPlDD3_&q=85&s=8eee81dc88436ec103986c38a85a6255" alt="Image showing permissions" width="940" height="357" data-path="modeling/advanced/assets/github-01.svg" />

To represent permissions in OpenFGA we use [relations](/docs/concepts#what-is-a-relation). For repository 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: 'repo',
    relations: {
      reader: {
        this: {},
      },
      triager: {
        this: {},
      },
      writer: {
        this: {},
      },
      maintainer: {
        this: {},
      },
      admin: {
        this: {},
      },
    },
    metadata: {
      relations: {
        reader: { directly_related_user_types: [{ type: 'user' }] },
        triager: { directly_related_user_types: [{ type: 'user' }] },
        writer: { directly_related_user_types: [{ type: 'user' }] },
        maintainer: { directly_related_user_types: [{ type: 'user' }] },
        admin: { 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_definitions: [
  {
    type: 'user',
  },
  {
    type: 'repo', // objects of type "repo"
    relations: {
      // have users related to them as
      reader: {
        // "reader", if those users belong to
        this: {}, // the userset of all users related to the repo as "reader"
      },
    },
    metadata: {
      relations: {
        reader: { directly_related_user_types: [{ type: '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"
</Info>

If we want to say `anne` is a reader of repository **repo:contoso/tooling** we create this [relationship tuple](/docs/concepts#what-is-a-relationship-tuple):

<WriteRequestViewer
  relationshipTuples={[
{
  user: 'user:anne',
  relation: 'reader',
  object: 'repo:contoso/tooling',
},
]}
/>

We can now [ask](/docs/concepts#what-is-a-check-request) OpenFGA "is `anne` a reader of repository **repo:contoso/tooling**?"

<CheckRequestViewer user={'user:anne'} relation={'reader'} object={'repo:contoso/tooling'} allowed={true} />

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

<WriteRequestViewer
  relationshipTuples={[
{
  user: 'user:beth',
  relation: 'writer',
  object: 'repo:contoso/tooling',
},
]}
/>

And ask some questions to OpenFGA:

<CheckRequestViewer user={'user:beth'} relation={'writer'} object={'repo:contoso/tooling'} allowed={true} />

<CheckRequestViewer user={'user:beth'} relation={'reader'} object={'repo:contoso/tooling'} allowed={false} />

The first reply makes sense but the second one does not. Intuitively, if `beth` was writer, she was also be a reader. In fact, GitHub explains this in [their documentation](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-organizations-and-teams/repository-permission-levels-for-an-organization#repository-access-for-each-permission-level)

<img src="https://mintcdn.com/fga/sFng5HcsvGPlDD3_/modeling/advanced/assets/github-repo-access-level.svg?fit=max&auto=format&n=sFng5HcsvGPlDD3_&q=85&s=08ec3920195005991bf6a95167301d51" alt="Showing various GitHub repo access level" width="1176" height="1608" data-path="modeling/advanced/assets/github-repo-access-level.svg" />

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

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'repo',
    relations: {
      reader: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'triager',
              },
            },
          ],
        },
      },
      triager: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'writer',
              },
            },
          ],
        },
      },
      writer: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'maintainer',
              },
            },
          ],
        },
      },
      maintainer: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'admin',
              },
            },
          ],
        },
      },
      admin: {
        this: {},
      },
    },
    metadata: {
      relations: {
        reader: { directly_related_user_types: [{ type: 'user' }] },
        triager: { directly_related_user_types: [{ type: 'user' }] },
        writer: { directly_related_user_types: [{ type: 'user' }] },
        maintainer: { directly_related_user_types: [{ type: 'user' }] },
        admin: { directly_related_user_types: [{ type: 'user' }] },
      },
    },
  },
],
}}
/>

Let us examine one of those relations in detail:

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type: 'repo', // objects of type "repo"
relations: {
  // have users related to them as
  reader: {
    // "reader": if they belong to
    union: {
      // any of (the union of) these user sets
      child: [
        {
          this: {}, // the userset of all users related to the repo as "reader"
        },
        {
          computedUserset: {
            // the userset of all users related to the repo as "triager"
            relation: 'triager',
          },
        },
      ],
    },
  },
},
metadata: {
  relations: {
    reader: { directly_related_user_types: [{ type: 'user' }] },
  },
},
}}
  skipVersion={true}
/>

<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](/docs/concepts#what-are-direct-and-implied-relationships) to the repo as a "reader"
  * the "triagers": the set of users who are related to the object as "triager"
</Info>

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

<CheckRequestViewer user={'user:beth'} relation={'writer'} object={'repo:contoso/tooling'} allowed={true} />

<CheckRequestViewer user={'user:beth'} relation={'reader'} object={'repo:contoso/tooling'} allowed={true} />

### 02. Permissions for teams in an org

GitHub also supports [creating teams in an organization](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-organizations-and-teams/creating-a-team), [adding members to a team](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-organizations-and-teams/adding-organization-members-to-a-team) and [granting teams permissions, rather than individuals](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-organizations-and-teams/managing-team-access-to-an-organization-repository).

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

<img src="https://mintcdn.com/fga/sFng5HcsvGPlDD3_/modeling/advanced/assets/github-02.svg?fit=max&auto=format&n=sFng5HcsvGPlDD3_&q=85&s=7589aa05499e8784f9bc11c2f1724de9" alt="Image showing permissions" width="940" height="578" data-path="modeling/advanced/assets/github-02.svg" />

To add support for teams and memberships all we need to do is add this object to the OpenFGA [authorization model](/docs/concepts#what-is-an-authorization-model):

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

In addition, the repo's relations should have team member as a directly related user types.

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'repo',
    relations: {
      reader: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'triager',
              },
            },
          ],
        },
      },
      triager: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'writer',
              },
            },
          ],
        },
      },
      writer: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'maintainer',
              },
            },
          ],
        },
      },
      maintainer: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'admin',
              },
            },
          ],
        },
      },
      admin: {
        this: {},
      },
    },
    metadata: {
      relations: {
        reader: { directly_related_user_types: [{ type: 'user' }, { type: 'team', relation: 'member' }] },
        triager: { directly_related_user_types: [{ type: 'user' }, { type: 'team', relation: 'member' }] },
        writer: { directly_related_user_types: [{ type: 'user' }, { type: 'team', relation: 'member' }] },
        maintainer: { directly_related_user_types: [{ type: 'user' }, { type: 'team', relation: 'member' }] },
        admin: { directly_related_user_types: [{ type: 'user' }, { type: 'team', relation: 'member' }] },
      },
    },
  },
],
}}
/>

Let us now create a team, add a member to it and make it an admin of **repo:contoso/tooling** by adding the following [relationship tuples](/docs/concepts#what-is-a-relationship-tuple):

<WriteRequestViewer
  relationshipTuples={[
{
  _description: 'make charles a member of the contoso/engineering team',
  user: 'user:charles',
  relation: 'member',
  object: 'team:contoso/engineering',
},
{
  _description: 'make members of contoso/engineering team admins of contoso/tooling',
  user: 'team:contoso/engineering#member',
  relation: 'admin',
  object: 'repo:contoso/tooling',
},
]}
/>

The last relationship tuple introduces a new **OpenFGA** concept. A **[userset](/docs/concepts#what-is-a-user)**. When the value of a user is formatted like this **type:objectId#relation**, OpenFGA will automatically expand the userset into all its individual user identifiers:

<CheckRequestViewer user={'user:charles'} relation={'admin'} object={'repo:contoso/tooling'} allowed={true} />

### 03. Permissions for child teams in an org

GitHub also supports team nesting, [known as "child teams"](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-organizations-and-teams/requesting-to-add-a-child-team). **Child teams inherit the access permissions of the parent team.**
Let's say we have a **protocols** team that is part of the **engineering**. The simplest way to achieve the aforementioned requirement is just adding this [relationship tuple](/docs/concepts#what-is-a-relationship-tuple):

<WriteRequestViewer
  relationshipTuples={[
{
  user: 'team:contoso/protocols#member',
  relation: 'member',
  object: 'team:contoso/engineering',
},
]}
/>

which says that members of protocols are members of engineering.

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

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

<WriteRequestViewer
  relationshipTuples={[
{
  _description: 'make diane a member of the contoso/protocols team',
  user: 'user:diane',
  relation: 'member',
  object: 'team:contoso/protocols',
},
]}
/>

<CheckRequestViewer user={'user:diane'} relation={'admin'} object={'repo:contoso/tooling'} allowed={true} />

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

<img src="https://mintcdn.com/fga/sFng5HcsvGPlDD3_/modeling/advanced/assets/github-03.svg?fit=max&auto=format&n=sFng5HcsvGPlDD3_&q=85&s=208af58f00d9c4bc8fe8fb89956d174c" alt="Image showing permissions" width="940" height="925" data-path="modeling/advanced/assets/github-03.svg" />

### 04. Base permissions for org members

In GitHub, ["you can set base permissions that apply to all members of an organization when accessing any of the organization's repositories"](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-organizations-and-teams/setting-base-permissions-for-an-organization). For our purposes this means that if:

* User `erik` is a member of an organization `contoso`
* *and* `contoso` has a repository `tooling`
* *and* `contoso` has configured base permission to be "write"

then `erik` has write permissions to tooling.

Let us model that!

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

<img src="https://mintcdn.com/fga/sFng5HcsvGPlDD3_/modeling/advanced/assets/github-04.svg?fit=max&auto=format&n=sFng5HcsvGPlDD3_&q=85&s=44faa37960b5012e1403fc073de012ef" alt="Diagram showing GitHub organization base permissions" width="1205" height="925" data-path="modeling/advanced/assets/github-04.svg" />

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

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

And support for repositories having owners:

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'repo',
    relations: {
      reader: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'triager',
              },
            },
          ],
        },
      },
      triager: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'writer',
              },
            },
          ],
        },
      },
      writer: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'maintainer',
              },
            },
          ],
        },
      },
      maintainer: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'admin',
              },
            },
          ],
        },
      },
      admin: {
        this: {},
      },
      owner: {
        // An organization can "own" a repository
        this: {},
      },
    },
    metadata: {
      relations: {
        reader: {
          directly_related_user_types: [{ type: 'user' }, { type: 'team', relation: 'member' }],
        },
        triager: {
          directly_related_user_types: [{ type: 'user' }, { type: 'team', relation: 'member' }],
        },
        writer: {
          directly_related_user_types: [{ type: 'user' }, { type: 'team', relation: 'member' }],
        },
        maintainer: {
          directly_related_user_types: [{ type: 'user' }, { type: 'team', relation: 'member' }],
        },
        admin: {
          directly_related_user_types: [{ type: 'user' }, { type: 'team', relation: 'member' }],
        },
        owner: {
          directly_related_user_types: [{ type: 'organization' }],
        },
      },
    },
  },
],
}}
/>

<Info>
  Note the added "owner" relation, indicating that organizations can own repositories.
</Info>

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

<WriteRequestViewer
  relationshipTuples={[
{
  user: 'user:erik',
  relation: 'member',
  object: 'organization:contoso',
},
{
  user: 'organization:contoso',
  relation: 'owner',
  object: 'repo:contoso/tooling',
},
]}
/>

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

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

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

<CheckRequestViewer user={'user:erik'} relation={'admin'} object={'repo:contoso/tooling'} />

More details on this technique can be found in the section [Modeling Parent-Child Objects](/docs/modeling/parent-child).

We express it like this:

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'repo',
    relations: {
      admin: {
        union: {
          child: [
            {
              this: {},
            },
            {
              tupleToUserset: {
                // read all tuples related to tooling as owner
                // which returns [{ "user": "organization:contoso", "relation": "owner", "object": "repo:contoso/tooling" }]
                tupleset: {
                  relation: 'owner',
                },
                // and for each tuple return all usersets that match the following, replacing $TUPLE_USERSET_OBJECT with organization:contoso
                // this will return tuples of shape { object: "organization:contoso", "repo_admin", "user": ??? }
                computedUserset: {
                  relation: 'repo_admin',
                },
              },
            },
          ],
        },
      },
    },
    metadata: {
      relations: {
        admin: {
          directly_related_user_types: [
            { type: 'user' },
            { type: 'team', relation: 'member' },
            { type: 'organization', relation: 'member' },
          ],
        },
      },
    },
  },
],
}}
  skipVersion={true}
/>

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

  * the "admins": the set of users who are [directly related](/docs/concepts#what-are-direct-and-implied-relationships) to the repo as an "admin"
  * the "repository admins of the org that owns the repo": from the objects who are related to the doc as owner, return the sets of users who are related to those objects as "repo\_admin"

  What the added section is doing is:

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

  `[{ "object": "repo:contoso/tooling", "relation": "owner", "user": "organization:contoso" }]`

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

  `{ "object": "organization:contoso", "relation": "repo_admin", "user": ??? }`
</Info>

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

* Well:
  * If the base permission for org contoso is repo\_admin then it should be **organization:contoso#member**.
  * If the base permission for org contoso is NOT repo\_admin, then it should be empty (no relationship tuple).
* Whenever the value of this dropdown changes:
  <img src="https://mintcdn.com/fga/sFng5HcsvGPlDD3_/modeling/advanced/assets/github-org-base-permissions-drop-down.png?fit=max&auto=format&n=sFng5HcsvGPlDD3_&q=85&s=ccd10176f36f704ce4d6c68ec54da3d9" alt="Selecting new permission level from base permissions drop-down" width="2514" height="1916" data-path="modeling/advanced/assets/github-org-base-permissions-drop-down.png" />
  * Delete the previous relationship tuple and create a new one:
    <WriteRequestViewer
      relationshipTuples={[
    {
      user: 'organization:contoso#member',
      relation: 'repo_admin',
      object: 'organization:contoso',
    },
    ]}
    />

The updated authorization model looks like this:

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'repo',
    relations: {
      admin: {
        union: {
          child: [
            {
              this: {},
            },
            {
              tupleToUserset: {
                computedUserset: {
                  relation: 'repo_admin',
                },
                tupleset: {
                  relation: 'owner',
                },
              },
            },
          ],
        },
      },
      maintainer: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'admin',
              },
            },
          ],
        },
      },
      writer: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'maintainer',
              },
            },
            {
              tupleToUserset: {
                computedUserset: {
                  relation: 'writer',
                },
                tupleset: {
                  relation: 'owner',
                },
              },
            },
          ],
        },
      },
      triager: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'writer',
              },
            },
          ],
        },
      },
      reader: {
        union: {
          child: [
            {
              this: {},
            },
            {
              computedUserset: {
                relation: 'triager',
              },
            },
            {
              tupleToUserset: {
                computedUserset: {
                  relation: 'reader',
                },
                tupleset: {
                  relation: 'owner',
                },
              },
            },
          ],
        },
      },
      owner: {
        this: {},
      },
    },
    metadata: {
      relations: {
        reader: {
          directly_related_user_types: [
            { type: 'user' },
            { type: 'team', relation: 'member' },
            { type: 'organization', relation: 'member' },
          ],
        },
        triager: {
          directly_related_user_types: [
            { type: 'user' },
            { type: 'team', relation: 'member' },
            { type: 'organization', relation: 'member' },
          ],
        },
        writer: {
          directly_related_user_types: [
            { type: 'user' },
            { type: 'team', relation: 'member' },
            { type: 'organization', relation: 'member' },
          ],
        },
        maintainer: {
          directly_related_user_types: [
            { type: 'user' },
            { type: 'team', relation: 'member' },
            { type: 'organization', relation: 'member' },
          ],
        },
        admin: {
          directly_related_user_types: [
            { type: 'user' },
            { type: 'team', relation: 'member' },
            { type: 'organization', relation: 'member' },
          ],
        },
        owner: {
          directly_related_user_types: [{ type: 'organization' }],
        },
      },
    },
  },
  {
    type: 'organization',
    relations: {
      owner: {
        this: {},
      },
      repo_admin: {
        this: {},
      },
    },
    metadata: {
      relations: {
        owner: {
          directly_related_user_types: [{ type: 'organization' }],
        },
        repo_admin: {
          directly_related_user_types: [
            { type: 'user' },
            { type: 'team', relation: 'member' },
            { type: 'organization', relation: 'member' },
          ],
        },
      },
    },
  },
],
}}
/>

## Summary

GitHub has a number of other permissions. You have [organization billing managers, users that can manage specific apps, etc](https://docs.github.com/en/free-pro-team@latest/github/setting-up-and-managing-organizations-and-teams/permission-levels-for-an-organization). We might explore those in the future, but hopefully this blog post has shown you how you could represent those cases using OpenFGA.


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