> ## 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 Entitlements for a System with OpenFGA

> Modeling entitlements for a system

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 entitlements for a platform like GitHub using OpenFGA.

<Note>
  **What you will learn**

  * How to model an entitlement use case in OpenFGA
  * How to start with a given set of requirements and scenarios and iterate on the OpenFGA model until those requirements are met
</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-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 members of an org are subscriber members of the plan the org is subscriber to, and subscriber members of a plan get access to all the plan's features.

  <h4 id="direct-relationships">Direct relationships</h4>

  You need to know how to disallow granting direct relation to an object and requiring the user to have a relation with another object that would imply a relation with the first one. [Learn more →](/docs/modeling/building-blocks/direct-relationships)

  Used here to indicate that "access" to a feature cannot be directly granted to a user, but is implied through the users organization subscribing to a plan that offers that feature.

  <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

In many product offerings, the features are behind multiple tiers. In this tutorial, you will build an authorization model for a subset of [GitHub's entitlements](https://github.com/pricing) (detailed below) using OpenFGA. You will use some scenarios to validate the model.

<img src="https://mintcdn.com/fga/sFng5HcsvGPlDD3_/modeling/advanced/assets/entitlements-image-pricing-github.svg?fit=max&auto=format&n=sFng5HcsvGPlDD3_&q=85&s=b224ef28ea59bc2aa8dd85843fd4898b" alt="GitHub Pricing Plan" width="1200" height="763" data-path="modeling/advanced/assets/entitlements-image-pricing-github.svg" />

At their core, entitlements is just asking: does a user X have access to feature Y? In GitHub's case for example, they have a concept called "Draft Pull Requests". Once the user loads the Pull Request page, the frontend needs to know whether it can show the "Draft Pull Request" option, as in it needs to know: "Does the current user have access to feature Draft Pull Request?".

<img src="https://mintcdn.com/fga/sFng5HcsvGPlDD3_/modeling/advanced/assets/entitlements-image-github-draft-pr.svg?fit=max&auto=format&n=sFng5HcsvGPlDD3_&q=85&s=4d20e2809a01f259cbfdd582b88ba655" alt="GitHub PR Page with Draft Pull Request" width="1796" height="756" data-path="modeling/advanced/assets/entitlements-image-github-draft-pr.svg" />

<img src="https://mintcdn.com/fga/sFng5HcsvGPlDD3_/modeling/advanced/assets/entitlements-image-github-no-draft-pr.svg?fit=max&auto=format&n=sFng5HcsvGPlDD3_&q=85&s=28e8a1f2ef02c2b63080852dd3e47243" alt="GitHub PR Page without Draft Pull Request" width="1796" height="756" data-path="modeling/advanced/assets/entitlements-image-github-no-draft-pr.svg" />

> Note: For brevity, this tutorial will not model all of GitHub entitlements. Instead, it will focus on modeling for the scenarios outlined below

### Requirements

You will model an entitlement system similar to GitHub's, focusing on a few scenarios.

GitHub has 3 plans: "Free", "Team" and "Enterprise", with each of them offering several features. The higher-priced plans include all the features of the lower priced plans. You will be focusing on a subset of the features offered.

A summary of GitHub's entitlement system:

* Free
  * Issues
* Team
  * *Everything from the free plan*
  * Draft Pull Requests
* Enterprise
  * *Everything from the team plan*
  * SAML Single Sign-On

### Defined scenarios

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

* Take these three organizations

  * Alpha Beta Gamma (`alpha`), a **subscriber** on the **free** plan
  * Bayer Water Supplies (`bayer`), a **subscriber** on the **team** plan
  * Cups and Dishes (`cups`), a **subscriber** on the **enterprise** plan

* Take these three users
  * **Anne**, **member** of **Alpha Beta Gamma**
  * **Beth**, **member** of **Bayer Water Supplies**
  * **Charles**, **member** of **Cups and Dishes**

<img src="https://mintcdn.com/fga/sFng5HcsvGPlDD3_/modeling/advanced/assets/entitlements-requirements.svg?fit=max&auto=format&n=sFng5HcsvGPlDD3_&q=85&s=2cbb9f260300da0bf9b40efabbbfb67b" alt="Image showing requirements" width="940" height="750" data-path="modeling/advanced/assets/entitlements-requirements.svg" />

By the end of this tutorial, you should be able to query OpenFGA with queries like:

* **Anne** has access to **Issues** (expecting `yes`)
* **Anne** has access to **Draft Pull Requests** (expecting` no`)
* **Anne** has access to **Single Sign-on** (expecting` no`)
* **Beth** has access to **Issues** (expecting `yes`)
* **Beth** has access to **Draft Pull Requests** (expecting `yes`)
* **Beth** has access to **Single Sign-on** (expecting` no`)
* **Charles** has access to **Issues** (expecting `yes`)
* **Charles** has access to **Draft Pull Requests** (expecting `yes`)
* **Charles** has access to **Single Sign-on** (expecting `yes`)

## Modeling entitlements for GitHub

### 01. Building The Initial Authorization Model And Relationship Tuples

In this tutorial you are going to take a different approach to previous tutorials. You will start with a simple [authorization model](/docs/concepts#what-is-an-authorization-model), add [relationship tuples](/docs/concepts#what-is-a-relationship-tuple) to represent some sample scenarios, and iterate until those scenarios return the results you expect.

In the scenarios outlined above, you have `organizations`, `plans` and `features`.

Similar to the example above, start with a basic listing of the types and their relations:

* A `feature` has a `plan` associated to it, we'll call the relation between them `associated_plan`
* A `plan` has an organization as a `subscriber` to it
* An `organization` has users as `members`

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'feature',
    relations: {
      associated_plan: {
        this: {},
      },
    },
    metadata: {
      relations: {
        associated_plan: { directly_related_user_types: [{ type: 'plan' }] },
      },
    },
  },
  {
    type: 'plan',
    relations: {
      subscriber: {
        this: {},
      },
    },
    metadata: {
      relations: {
        subscriber: { directly_related_user_types: [{ type: 'organization' }] },
      },
    },
  },
  {
    type: 'organization',
    relations: {
      member: {
        this: {},
      },
    },
    metadata: {
      relations: {
        member: { directly_related_user_types: [{ type: 'user' }] },
      },
    },
  },
],
}}
/>

### 02. Populating the relationship tuples

Now you can add the relationship tuples to represent these relationships mentioned in the [requirements](#requirements) and [scenarios](#defined-scenarios) sections:

The relations between the features and plans are as follows:

<WriteRequestViewer
  relationshipTuples={[
{
  _description: 'the free plan is the associated plan of the issues feature',
  user: 'plan:free',
  relation: 'associated_plan',
  object: 'feature:issues',
},
{
  _description: 'the team plan is the associated plan of the issues feature',
  user: 'plan:team',
  relation: 'associated_plan',
  object: 'feature:issues',
},
{
  _description: 'the team plan is the associated plan of the draft pull requests feature',
  user: 'plan:team',
  relation: 'associated_plan',
  object: 'feature:draft_prs',
},
{
  _description: 'the enterprise plan is the associated plan of the issues feature',
  user: 'plan:enterprise',
  relation: 'associated_plan',
  object: 'feature:issues',
},
{
  _description: 'the enterprise plan is the associated plan of the draft pull requests feature',
  user: 'plan:enterprise',
  relation: 'associated_plan',
  object: 'feature:draft_prs',
},
{
  _description: 'the enterprise plan is the associated plan of the SAML Single Sign-on feature',
  user: 'plan:enterprise',
  relation: 'associated_plan',
  object: 'feature:sso',
},
]}
/>

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

<WriteRequestViewer
  relationshipTuples={[
{
  _description: 'the Alpha Beta Gamma organization is a subscriber of the free plan',
  user: 'organization:alpha',
  relation: 'subscriber',
  object: 'plan:free',
},
{
  _description: 'the Bayer Water Supplies organization is a subscriber of the team plan',
  user: 'organization:bayer',
  relation: 'subscriber',
  object: 'plan:team',
},
{
  _description: 'the Cups and Dishes organization is a subscriber of the enterprise plan',
  user: 'organization:cups',
  relation: 'subscriber',
  object: 'plan:enterprise',
},
]}
/>

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

<WriteRequestViewer
  relationshipTuples={[
{
  _description: 'anne is a member of the Alpha Beta Gamma organization',
  user: 'user:anne',
  relation: 'member',
  object: 'organization:alpha',
},
{
  _description: 'beth is a member of the Bayer Water Supplies',
  user: 'user:beth',
  relation: 'member',
  object: 'organization:bayer',
},
{
  _description: 'charles is a member of the Cups and Dishes organization',
  user: 'user:charles',
  relation: 'member',
  object: 'organization:cups',
},
]}
/>

So far you have given OpenFGA a representation of the current state of your system's relationships. You will keep iterating and updating the authorization model until the results of the queries match what you expect.

<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.
  For example, the relationship tuple indicating that *anne is a member of organization:alpha* could be written as:

  * user: user:2b4840f2-7c9c-42c8-9329-911002051524
  * relation: member
  * object: project:52e529c6-c571-4d5c-b78a-bc574cf98b54
</Warning>

#### Verification

Now that you have some data, you can start using it to ask is `${USER}` related to `${OBJECT}` as `${RELATION}`?

First, you will [check](/docs/concepts#what-is-a-check-request) if `anne` is a member of `organization:alpha`. This is one of the relationship tuples you previously added, you will make sure OpenFGA can detect a relation in this case.

<CheckRequestViewer user={'user:anne'} relation={'member'} object={'organization:alpha'} allowed={true} />

Querying for relationship tuples that you fed into OpenFGA earlier should work, try a few before proceeding to make sure everything is working well.

<CheckRequestViewer user={'user:anne'} relation={'member'} object={'organization:bayer'} allowed={false} />

<CheckRequestViewer user={'organization:bayer'} relation={'subscriber'} object={'plan:team'} allowed={true} />

<CheckRequestViewer user={'plan:free'} relation={'associated_plan'} object={'feature:issues'} allowed={true} />

### 03. Updating the authorization model

You are working towards OpenFGA returning the correct answer when you query whether `anne` has `access` to `feature:issues`. It won't work yet, but you will keep updating your configuration to reach that goal.

To start, try to run that query on `is anne related to feature:issues as access?`

<CheckRequestViewer user={'user:anne'} relation={'access'} object={'feature:issues'} />

The OpenFGA service is returning that the query tuple is invalid. That is because you are asking for relation as `access`, but that relation is not in the configuration of the `feature` type!

Add it now. Like so:

<AuthzModelSnippetViewer
  configuration={{
type: 'feature',
relations: {
  associated_plan: {
    this: {},
  },
  access: {
    this: {},
  },
},
metadata: {
  relations: {
    associated_plan: { directly_related_user_types: [{ type: 'plan' }] },
    access: { directly_related_user_types: [{ type: 'user' }] },
  },
},
}}
  skipVersion={true}
/>

<Info>
  `access` [relation](/docs/concepts#what-is-a-relation) was added to the configuration of the `feature` [type](/docs/concepts#what-is-a-type).
</Info>

<Note>
  In this tutorial, you will find the phrases [direct relationship and implied relationship](/docs/concepts#what-are-direct-and-implied-relationships).

  A *direct relationship* R between user X and object Y means the relationship tuple (user=X, relation=R, object=Y) exists, and the OpenFGA authorization model for that relation allows this direct relationship (by use of [direct relationship type restrictions](/docs/configuration-language#direct-relationship-type-restrictions)).

  An *implied relationship* R exists between user X and object Y if user X is related to an object Z that is in direct or implied relationship with object Y, and the OpenFGA authorization model allows it.
</Note>

The resulting updated configuration would be:

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'feature',
    relations: {
      associated_plan: {
        this: {},
      },
      access: {
        this: {},
      },
    },
    metadata: {
      relations: {
        associated_plan: { directly_related_user_types: [{ type: 'plan' }] },
        access: { directly_related_user_types: [{ type: 'user' }] },
      },
    },
  },
  {
    type: 'plan',
    relations: {
      subscriber: {
        this: {},
      },
    },
    metadata: {
      relations: {
        subscriber: { directly_related_user_types: [{ type: 'organization' }] },
      },
    },
  },
  {
    type: 'organization',
    relations: {
      member: {
        this: {},
      },
    },
    metadata: {
      relations: {
        member: { directly_related_user_types: [{ type: 'user' }] },
      },
    },
  },
],
}}
/>

#### Adding modeling pattern of parent-child objects

Now we can ask the following query: `is anne related to feature:issues as access?` again.

<CheckRequestViewer user={'user:anne'} relation={'access'} object={'feature:issues'} allowed={false} />

So far so good. OpenFGA understood your query, but said that no [relation](/docs/concepts#what-is-a-relation) exists. That is because according to the configuration provided so far, there is no `access` relation between `anne` and `feature:issues`.

We can also try to query `is organization:alpha related to feature:issues as access?` and we see that there is no relationship.

<CheckRequestViewer user={'organization:alpha'} relation={'access'} object={'feature:issues'} allowed={false} />

If you have already completed some of the other tutorials you might have encountered the modeling pattern of [parent-child objects](/docs/modeling/parent-child) which is modeled as such:

<AuthzModelSnippetViewer
  configuration={{
type: 'resource',
relations: {
  viewer: {
    tupleToUserset: {
      tupleset: {
        relation: 'parent',
      },
      computedUserset: {
        relation: 'all_objects_viewer',
      },
    },
  },
},
}}
  skipVersion={true}
/>

<Info>
  With this, when asked to check a user's `viewer` relationship with the object, OpenFGA will:

  1. Read all relationship tuples of users related to this particular object as relation `parent`
  2. For each relationship tuple, return all *usersets* that have `all_objects_viewer` relation to the objects in those relationship tuples
  3. If the user is in any of those *usersets*, return yes, as the user is a `viewer` on this object.
     In other words, users related as `all_objects_viewer` to any of this object's `parents` are related as `viewer` to this object.
</Info>

If you want to give all subscribers on a plan access to a feature, you can do it like so:

<AuthzModelSnippetViewer
  configuration={{
type: 'feature',
relations: {
  associated_plan: {
    this: {},
  },
  access: {
    union: {
      child: [
        {
          this: {},
        },
        {
          tupleToUserset: {
            tupleset: {
              relation: 'associated_plan',
            },
            computedUserset: {
              relation: 'subscriber',
            },
          },
        },
      ],
    },
  },
},
metadata: {
  relations: {
    associated_plan: { directly_related_user_types: [{ type: 'plan' }] },
    access: {
      directly_related_user_types: [{ type: 'user' }],
    },
  },
},
}}
  skipVersion={true}
/>

<Info>
  Users related to `feature` as `access` are the union of (any of):

  * the set of users with a direct `access` relation
  * the set of users related to the `associated_plan` as `subscriber` (the feature's associated plans' subscribers)

  So everyone who has direct access, as well as the subscribers of the associated plan
</Info>

That would mean that in order for an object to have an `access` relation to a feature y, there needs to be either:

* a [direct relationship](/docs/concepts#what-are-direct-and-implied-relationships) via a relationship tuple:
  e.g. `{ "user": "user:x", "relation": "access", "object": "feature:y" }`
* a subscriber relationship with another object related to x associated\_plan:
  e.g. `{ "user": "user:x", "relation": "subscriber", "object": "plan:z" } { "user": "plan:z", "relation": "associated_plan", "object": "feature:y" }`

That brings you close. That will allow you to grant organizations access to the feature (as organizations have a subscriber relation with the plan).

#### Adding Subscriber Relationship With Another Object Related To x associated\_plan

One way forward would be to add a direct `access` relation between a user and a feature e.g. `{ "user": "anne", "relation": "access", "object": "feature:y" }` whenever the organization anne is subscribed to a plan, or the organization anne is in subscribes to a new plan.
But there are several downsides to this:

* Your application layer now needs to worry about computing this relationship. Instead of letting OpenFGA figure this all out, the app layer needs to do the checks whenever a user is being added or removed
* If an organization changes its subscription, your application layer has to loop through all the users and update their `access` relationships to features accordingly

Later in this tutorial, you will remove the possibility of having a direct `access` relation completely, but for now you will make sure the changes to the store you have made so far are working.

Replace all the existing code you had previously with the updated authorization model from the below snippet.

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'feature',
    relations: {
      associated_plan: {
        this: {},
      },
      access: {
        union: {
          child: [
            {
              this: {},
            },
            {
              tupleToUserset: {
                tupleset: {
                  relation: 'associated_plan',
                },
                computedUserset: {
                  relation: 'subscriber',
                },
              },
            },
          ],
        },
      },
    },
    metadata: {
      relations: {
        associated_plan: { directly_related_user_types: [{ type: 'plan' }] },
        access: {
          directly_related_user_types: [{ type: 'user' }],
        },
      },
    },
  },
  {
    type: 'plan',
    relations: {
      subscriber: {
        this: {},
      },
    },
    metadata: {
      relations: {
        subscriber: { directly_related_user_types: [{ type: 'organization' }] },
      },
    },
  },
  {
    type: 'organization',
    relations: {
      member: {
        this: {},
      },
    },
    metadata: {
      relations: {
        member: { directly_related_user_types: [{ type: 'user' }] },
      },
    },
  },
],
}}
/>

Now we can ask following query: `is organization:alpha related to feature:issues as access?` again.

<CheckRequestViewer user={'organization:alpha'} relation={'access'} object={'feature:issues'} allowed={true} />

You will notice that OpenFGA now did find a relation, as `organization:alpha` is a `subscriber` to `plan:free` which has an `associated_plan` relation to `feature:issues`. From that and the authorization model you updated above, OpenFGA deduced that `organization:alpha` has an implied `access` relation to `feature:issues`.

That is good, but you want to be able to ask `is anne related to feature:issues as access?`, not `is organization:alpha related to feature:issues as access?`. As in, you want the subscriber members to have access to the feature, not the subscriber itself.

In order to do that, you will add a relation on the plan, that indicates that all members of an organization subscribed to it, have a `subscriber_member` relation to the plan. And you can modify the change you did above to give implied access to the `subscriber_member` instead of the subscriber. Like so:

<AuthzModelSnippetViewer
  configuration={{
schema_version: '1.1',
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'feature',
    relations: {
      associated_plan: {
        this: {},
      },
      access: {
        union: {
          child: [
            {
              this: {},
            },
            {
              tupleToUserset: {
                tupleset: {
                  relation: 'associated_plan',
                },
                computedUserset: {
                  relation: 'subscriber_member',
                },
              },
            },
          ],
        },
      },
    },
    metadata: {
      relations: {
        associated_plan: { directly_related_user_types: [{ type: 'plan' }] },
        access: {
          directly_related_user_types: [{ type: 'user' }],
        },
      },
    },
  },
  {
    type: 'plan',
    relations: {
      subscriber: {
        this: {},
      },
      subscriber_member: {
        tupleToUserset: {
          tupleset: {
            relation: 'subscriber',
          },
          computedUserset: {
            relation: 'member',
          },
        },
      },
    },
    metadata: {
      relations: {
        subscriber: { directly_related_user_types: [{ type: 'organization' }] },
      },
    },
  },
  {
    type: 'organization',
    relations: {
      member: {
        this: {},
      },
    },
    metadata: {
      relations: {
        member: { directly_related_user_types: [{ type: 'user' }] },
      },
    },
  },
],
}}
/>

<Info>
  Notice that `subscriber` has been updated to `subscriber_member` in the `access` relation of the `feature` type.

  Under the `plan` type, in order for someone to have a `subscriber_member` relation to the plan, they have to be related as a `member` to the object related as a `subscriber` to the plan (as in they have to be a member of on of the plan's subscribers).
</Info>

Now ask the following query: `is anne related to feature:issues as access?`

<CheckRequestViewer user={'user:anne'} relation={'access'} object={'feature:issues'} allowed={true} />

#### Disallow direct relationship

So far, with just a OpenFGA authorization model, and the initial relationship tuples indicating the relations you know, you configured OpenFGA to give you the correct response.

Earlier on, the idea of not allowing a direct `access` relation between a user and a `feature` was discussed, e.g. adding a relationship tuple like `{ "user": "user:anne", "relation": "access", "object": "feature:y" }`. You will remove it now.

To disallow a direct relationship, you need to remove the direct relationship type restriction. The following snippet:

<AuthzModelSnippetViewer
  configuration={{
type: 'feature',
relations: {
  associated_plan: {
    this: {},
  },
  access: {
    union: {
      child: [
        {
          this: {},
        },
        {
          tupleToUserset: {
            tupleset: {
              relation: 'associated_plan',
            },
            computedUserset: {
              relation: 'subscriber_member',
            },
          },
        },
      ],
    },
  },
},
metadata: {
  relations: {
    associated_plan: { directly_related_user_types: [{ type: 'plan' }] },
    access: {
      directly_related_user_types: [{ type: 'user' }],
    },
  },
},
}}
  skipVersion={true}
/>

becomes

<AuthzModelSnippetViewer
  configuration={{
type: 'feature',
relations: {
  associated_plan: {
    this: {},
  },
  access: {
    tupleToUserset: {
      tupleset: {
        relation: 'associated_plan',
      },
      computedUserset: {
        relation: 'subscriber_member',
      },
    },
  },
},
metadata: {
  relations: {
    associated_plan: { directly_related_user_types: [{ type: 'plan' }] },
  },
},
}}
  skipVersion={true}
/>

With this change, even if your app layer added the following relationship tuple:

* `{ "user": "user:anne", "relation": "access", "object": "feature:issues" }`

a subsequent check for `is anne related to feature:issues as access?` would return no relation. The only way for a relation to exist is if the following three relationship tuples do:

* `{ "user": "user:anne", "relation": "member", "object": "organization:z" }`
* `{ "user": "organization:z", "relation": "subscriber", "object": "plan:y" }`
* `{ "user": "plan:y", "relation": "associated_plan", "object": "feature:issues" }`

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

Ensure that your authorization model matches the one below

<AuthzModelSnippetViewer
  configuration={{
type_definitions: [
  {
    type: 'user',
  },
  {
    type: 'feature',
    relations: {
      associated_plan: {
        this: {},
      },
      access: {
        tupleToUserset: {
          tupleset: {
            relation: 'associated_plan',
          },
          computedUserset: {
            relation: 'subscriber_member',
          },
        },
      },
    },
    metadata: {
      relations: {
        associated_plan: { directly_related_user_types: [{ type: 'plan' }] },
      },
    },
  },
  {
    type: 'plan',
    relations: {
      subscriber: {
        this: {},
      },
      subscriber_member: {
        tupleToUserset: {
          tupleset: {
            relation: 'subscriber',
          },
          computedUserset: {
            relation: 'member',
          },
        },
      },
    },
    metadata: {
      relations: {
        subscriber: { directly_related_user_types: [{ type: 'organization' }] },
      },
    },
  },
  {
    type: 'organization',
    relations: {
      member: {
        this: {},
      },
    },
    metadata: {
      relations: {
        member: { directly_related_user_types: [{ type: 'user' }] },
      },
    },
  },
],
}}
/>

You will now verify that the configuration is correct by running checks for all the scenarios mentioned at the beginning of the tutorial:

* **Anne** has access to **Issues** (expecting `yes`)
* **Anne** has access to **Draft Pull Requests** (expecting` no`)
* **Anne** has access to **Single Sign-on** (expecting` no`)
* **Beth** has access to **Issues** (expecting `yes`)
* **Beth** has access to **Draft Pull Requests** (expecting `yes`)
* **Beth** has access to **Single Sign-on** (expecting` no`)
* **Charles** has access to **Issues** (expecting `yes`)
* **Charles** has access to **Draft Pull Requests** (expecting `yes`)
* **Charles** has access to **Single Sign-on** (expecting `yes`)

<CheckRequestViewer user={'user:anne'} relation={'access'} object={'feature:issues'} allowed={true} />

Try to verify for the other user, object and relation combinations as listed below.

| User | Object | Relation | Query | Relation? |
| - | - | - | - | - |
| `anne` | `feature:issues` | `access` | `is anne related to feature:issues as access?` | Yes |
| `anne` | `feature:draft_prs` | `access` | `is anne related to feature:draft_prs as access?` | No |
| `anne` | `feature:sso` | `access` | `is anne related to feature:sso as access?` | No |
| `beth` | `feature:issues` | `access` | `is beth related to feature:issues as access?` | Yes |
| `beth` | `feature:draft_prs` | `access` | `is beth related to feature:draft_prs as access?` | Yes |
| `beth` | `feature:sso` | `access` | `is beth related to feature:sso as access?` | No |
| `charles` | `feature:issues` | `access` | `is charles related to feature:issues as access?` | Yes |
| `charles` | `feature:draft_prs` | `access` | `is charles related to feature:draft_prs as access?` | Yes |
| `charles` | `feature:sso` | `access` | `is charles related to feature:sso as access?` | Yes |

## Summary

In this tutorial, you learned:

* to model entitlements for a system in OpenFGA
* how to start with a set of requirements and scenarios and iterate on the OpenFGA authorization model until the checks match the expected scenarios
* how to model [**parent-child relationships**](/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
* how to use [**the union operator**](/docs/configuration-language#the-union-operator) condition to indicate multiple possible paths for a relationship between two objects to be computed
* using [**direct relationship type restrictions**](/docs/configuration-language#direct-relationship-type-restrictions) in a OpenFGA authorization model, and how to block direct relationships by removing it

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.


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