Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edit: Modeling Hierarchis #243

Merged
merged 1 commit into from
Dec 27, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 23 additions & 16 deletions astro/src/content/docs/extend/examples/modeling-hierarchies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

Let's take an example. Assume that you want to use FusionAuth to authorize users in your corporation, Change Corp, to have access to certain documents. Your corporation has two sub-companies: Change Bank and Change Insurance. Each company has many departments, like marketing, sales, finance, operations, and management. Documents belong to a single department in an organization. Companies, departments, and documents have read and write permissions.

Permissions propagate downwards. So an employee with write permissions to the marketing department in Change Insurance, will have write permissions to all its documents. And an employee with read permissions to Change Corp has read permissions to every document in every department of both sub-companies. But you might have an auditor who you add as a user in FusionAuth that has only read access to a specific document in a specific department. This will not give her permissions to any other documents anywhere higher in the organizational hierarchy.
Permissions propagate downwards. So an employee with write permissions to the marketing department in Change Insurance will have write permissions to all its documents. And an employee with read permissions to Change Corp has read permissions to every document in every department of both sub-companies. But you might have an auditor who you add as a user in FusionAuth that has only read access to a specific document in a specific department. This will not give her permissions to any other documents anywhere higher in the organizational hierarchy.

Below is a diagram of the company structure to model.

Expand All @@ -44,16 +44,22 @@
- How do you handle documents that everyone in the corporation needs to read, such as a corporate HR manual, which is managed by the HR department of the top-level corporation? Because permissions don't propagate upwards, you have to individually give read permissions to everyone, instead of relying on the hierarchy to do it automatically.
- What happens when permissions conflict? The operations department might have a passwords document that should have read access only by members of that department, but anyone with read access to the sub-company will have access to the passwords.

There are solutions to these problems, such as including "Deny access" permissions and a "Common" department which has shared documents, and you need to pick what works for your organization. These challenges won't be discussed in this guide, as you can use the techniques shown here to implement your own solution.
There are solutions to these problems, such as including "Deny access" permissions and a "Common" department for shared documents, and you need to pick what works for your organization. These challenges won't be discussed in this guide, as you can use the techniques shown here to implement your own solution.
</Aside>

## How To Model Hierarchy In FusionAuth With Entities And Grants

The best way to model a hierarchy in FusionAuth is with Entities. For the example above, you should create entity types Company and Department with permissions Read, Write, and IsMember. Read and write are used to show permissions, but IsMember is used to show hierarchy. Create an entity called Change Bank of type Company and entity of Department called Operations. Create an entity grant for Operations to Change Bank with IsMember set to true to show that this Operations entity belongs to the Change Bank entity. Note that it will not be possible to tell departments called Operations in different companies apart by their name alone. You will need to examine each department's entity grant to see which company it belongs to. Create an entity grant for user Alice to entity Change Bank with no permissions, and an entity grant for Alice to Operations with permissions Read and Write. Below is a diagram of this example, which is similar to the earlier types diagram, but includes a department hierarchy now. Permissions are shown in separate blocks now too.
The best way to model a hierarchy in FusionAuth is with Entities.

For the example above, you should create entity types Company and Department with permissions Read, Write, and IsMember. Read and write are used to show permissions, but IsMember is used to show hierarchy.

Then create an entity called Change Bank of type Company and entity of Department called Operations. Create an entity grant for Operations to Change Bank with IsMember set to true to show that this Operations entity belongs to the Change Bank entity. Note that it will not be possible to tell departments called Operations in different companies apart by their name alone. You will need to examine each department's entity grant to see which company it belongs to.

Finally, you'll create an entity grant for user Alice to entity Change Bank with no permissions, and an entity grant for Alice to Operations with permissions Read and Write. Below is a diagram of this example, which is similar to the earlier types diagram, but includes a department hierarchy now. Permissions are shown in separate blocks now too.

![Hierarchy diagram](/img/docs/extend/examples/modeling-hierarchy/mermaid3.webp)

For simplicity's sake this diagram does not include Change Corp entity of entity type Corporation. There are two blocks: one for Change Insurance and one for Change Bank. Ignore the Change Insurance block and concentrate on Change Bank to see how Alice is connected to her department, which is connected to the company. This diagram also shows a document attached to the Operations department. The document itself has needs read and write permissions, for when you want to enable individual access, and is linked to the Operations department via an entity grant with the IsMember permission, in the same way departments are linked to companies.
For simplicity's sake, this diagram does not include the Change Corp entity of entity type Corporation. There are two blocks: one for Change Insurance and one for Change Bank. Ignore the Change Insurance block and concentrate on Change Bank to see how Alice is connected to her department, which is connected to the company. This diagram also shows a document attached to the Operations department. The document itself needs read and write permissions, for when you want to enable individual access, and is linked to the Operations department via an entity grant with the IsMember permission, in the same way departments are linked to companies.

Finally, note that you should use a matching UUID for every document in your document management system and in FusionAuth, to handle situations where document names and versions change.

Expand Down Expand Up @@ -82,7 +88,7 @@
```
</Aside>

This command started FusionAuth using Kickstart, which automatically creates an example application with an example user called Richard. It saves you the time of having to configure everything yourself when following this tutorial.
This command starts FusionAuth using Kickstart, which automatically creates an example application with an example user called Richard. It saves you the time of having to configure everything yourself when following this tutorial.

- Log in to your FusionAuth web interface at http://localhost:9011/admin with credentials `[email protected]` and `password`.
- Browse to <Breadcrumb>Reactor</Breadcrumb>.
Expand All @@ -94,7 +100,7 @@

### Create Hierarchy Entities

In this section you'll create the entities and permissions in FusionAuth to represent a company hierarchy with documents.
In this section, you'll create the entities and permissions in FusionAuth to represent a company hierarchy with documents.

- Browse to <Breadcrumb>Entity Management -> Entity Types</Breadcrumb>.
- Click the <InlineUIElement>+ Add</InlineUIElement> button.
Expand Down Expand Up @@ -158,11 +164,11 @@

### Run Your Website To Calculate All The User Permissions

In this section you'll write a script to get all the direct and indirect (through the company hierarchy) permissions a user has to all documents in FusionAuth. All you need is the user's email address or Id. Though this is a simple script, you can use exactly the same code after the user has logged in to your website with FusionAuth. (To learn how to make a simple Node.js website that uses FusionAuth, read the [quickstart](/docs/quickstarts/quickstart-javascript-express-web).)
In this section, you'll write a script to get all the direct and indirect (through the company hierarchy) permissions a user has to all documents in FusionAuth. All you need is the user's email address or Id. Though this is a simple script, you can use exactly the same code after the user has logged in to your website with FusionAuth. (To learn how to make a simple Node.js website that uses FusionAuth, read the [quickstart](/docs/quickstarts/quickstart-javascript-express-web).)

For this script, you'll use TypeScript. It's easy to make errors when working with a tree structure, like these parent and child entities. TypeScript's strong typing will prevent errors, and enable you to see exactly which properties are available on each object. If you prefer JavaScript, you can delete all the type syntax, rename the file with `.js`, and the code will still run fine.

Start by creating the script, called `getPermissions.ts` in the current `light` working directory, and add the type definitions below. Axios will be used to call the FusionAuth API on the network.

Check failure on line 171 in astro/src/content/docs/extend/examples/modeling-hierarchies.mdx

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Vale.Terms] Use 'axios' instead of 'Axios'. Raw Output: {"message": "[Vale.Terms] Use 'axios' instead of 'Axios'.", "location": {"path": "astro/src/content/docs/extend/examples/modeling-hierarchies.mdx", "range": {"start": {"line": 171, "column": 136}}}, "severity": "ERROR"}

```ts
import axios from "npm:[email protected]";
Expand Down Expand Up @@ -198,7 +204,7 @@

These types show all the objects returned when calling FusionAuth, listing only the important properties, and ignoring the other properties. An entity has only a name and a type. There are two types of grants, one for users and one for entities. Note the Id here points to the grant object itself, not the target entity. You usually want to use the Id of the entity inside the grant. A grant's permissions are an array of strings.

The permission type is what you're aiming to find as the goal of this script: a document (an entity with a name and Id) and all the permissions a user has to it. These permissions are a set, not an array, to avoid duplicates.
The goal of this script is to find the permission type: A document (an entity with a name and Id) and all the permissions a user has to it. These permissions are a set, not an array, to avoid duplicates.

Next, add a function to calculate the user's permissions to every document, which starts with code to get the user from FusionAuth, all entities, and all grants from every entity to every other.

Expand All @@ -218,7 +224,7 @@
}
```

Note that the FusionAuth API key is hardcoded into this file and passed to Axios. In reality, you should never commit your key to Git, but keep it in a `.env` file. The rest of the code is simple: it calls the FusionAuth API for each type and stores the result returned. Read more about the API for [users](/docs/apis/users), [entities](/docs/apis/entities/entities), and [grants](/docs/apis/entities/grants).
Note that the FusionAuth API key is hardcoded into this file and passed to Axios. In reality, you should never commit your key to Git, but keep it in a `.env` file. The rest of the code is straightforward: It calls the FusionAuth API for each type and stores the result returned. Read more about the APIs for [users](/docs/apis/users), [entities](/docs/apis/entities/entities), and [grants](/docs/apis/entities/grants).

Check failure on line 227 in astro/src/content/docs/extend/examples/modeling-hierarchies.mdx

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Vale.Terms] Use 'axios' instead of 'Axios'. Raw Output: {"message": "[Vale.Terms] Use 'axios' instead of 'Axios'.", "location": {"path": "astro/src/content/docs/extend/examples/modeling-hierarchies.mdx", "range": {"start": {"line": 227, "column": 76}}}, "severity": "ERROR"}

Check failure on line 227 in astro/src/content/docs/extend/examples/modeling-hierarchies.mdx

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Vale.Terms] Use 'axios' instead of 'Axios'. Raw Output: {"message": "[Vale.Terms] Use 'axios' instead of 'Axios'.", "location": {"path": "astro/src/content/docs/extend/examples/modeling-hierarchies.mdx", "range": {"start": {"line": 227, "column": 76}}}, "severity": "ERROR"}

Continue the function above by calculating the permissions for the user for every document, and end the script by calling the function.

Expand Down Expand Up @@ -262,9 +268,9 @@
await getUserPermissions('[email protected]');
```

This code is a little tricky if you haven't worked with a tree structure before. Luckily, our example assumes that every entity has only one owner (parent node), so in all searches, once you find a grant with `IsMember` you know you have found the node's only parent. The code starts by looping through every entity that is a document, because you can ignore entities that aren't documents. For each document, the code gets all ancestors (the document's department and the department's company). Then it finally checks if the user has any permissions to any of those entities, and adds the permissions to the list of permissions the user has to the document.
This code is a little tricky if you haven't worked with a tree structure before. Luckily, our example assumes that every entity has only one owner (parent node), so in all searches, once you find a grant with `IsMember`, you know you have found the node's only parent. The code starts by looping through every entity that is a document, because you can ignore entities that aren't documents. For each document, the code gets all ancestors (the document's department and the department's company). Then it finally checks if the user has any permissions to any of those entities, and adds the permissions to the list of permissions the user has to the document.

In a new terminal, run the commands below to install Axios and run the script to check what permissions Richard has to both documents. Here, to save time, you use Docker again, with the Deno 2 image, which can run TypeScript without any compile step, as well as allowing you to freely mix JavaScript, ES modules, and CommonJS modules. In reality, you could use the TypeScript compiler, and Node, Bun, or any other JavaScript environment you like.

Check failure on line 273 in astro/src/content/docs/extend/examples/modeling-hierarchies.mdx

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Vale.Terms] Use 'axios' instead of 'Axios'. Raw Output: {"message": "[Vale.Terms] Use 'axios' instead of 'Axios'.", "location": {"path": "astro/src/content/docs/extend/examples/modeling-hierarchies.mdx", "range": {"start": {"line": 273, "column": 54}}}, "severity": "ERROR"}

Check failure on line 273 in astro/src/content/docs/extend/examples/modeling-hierarchies.mdx

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'Deno'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'Deno'?", "location": {"path": "astro/src/content/docs/extend/examples/modeling-hierarchies.mdx", "range": {"start": {"line": 273, "column": 187}}}, "severity": "ERROR"}

```sh
docker run --platform=linux/amd64 --rm --network faNetwork -v ".:/app" -w "/app" denoland/deno:alpine-2.1.3 sh -c "deno run --allow-net --allow-read ./getPermissions.ts"
Expand Down Expand Up @@ -314,7 +320,7 @@

## Clean Up

To remove all the Docker volume, containers, images, and networks used in this guide, run the commands below.
To remove all the Docker volumes, containers, images, and networks used in this guide, run the commands below.

```sh
docker compose down -v
Expand All @@ -325,19 +331,20 @@

## Alternative Methods To Model Hierarchy In FusionAuth

The code above demonstrated entities, but there are a few ways to model the example company structure in FusionAuth. Documents have to be entities, or stored outside FusionAuth, and employees have to be users. There are no other types in FusionAuth that will work for this.
The code above demonstrates modeling hierarchies using entities, but there are other ways to model the example company structure in FusionAuth. Documents must be entities, or stored outside FusionAuth, and employees must be users. No other types in FusionAuth will work for this.

After that, below are the two alternatives to using entities and grants.
With these constraints in mind, below are the two alternatives to using entities and grants.

### Applications And Roles

In this option, you add the finance employee, Alice, to an application representing her company and department, like Change Bank Operations application, instead of an entity representing the company. Each application will have two roles, read and write, which are effectively permissions not roles. You can't use groups instead of applications to model this example because groups do not have permissions. Alice will need to be a member of multiple applications, for different departments and companies.

You would have to keep record of what department each document belongs to outside FusionAuth. This is a bad option that provides no benefit over using entities instead, unless you are using the free version of FusionAuth, which does not have entities.
You would have to keep record of what department each document belongs to outside FusionAuth. This approach offers no benefit over using entities, unless you are using the free version of FusionAuth, which does not have entities.

### User JSON Data

In this option, you store every user's company and department as properties in their JSON `user.data` field. This has to be done through the FusionAuth API, and cannot be maintained in the FusionAuth web interface. You will need to write your own UI app for HR staff to work with FusionAuth. With this approach you don't need to use applications, roles, or groups. Below is example JSON data for Alice:
In this option, you store every user's company and department as properties in their JSON `user.data` field. This has to be done through the FusionAuth API, and cannot be maintained in the FusionAuth web interface. You will need to write your own UI app for HR staff to work with FusionAuth. With this approach, you don't need to use applications, roles, or groups. Below is example JSON data for Alice:

```js
"permissions": {
"Change Bank": [],
Expand All @@ -350,4 +357,4 @@

Using entities, as the example script demonstrated earlier, or using JSON, are opposite approaches. Using entities explicitly stores the relationship between all organizational departments and all documents and their related permissions in FusionAuth. In contrast, using JSON doesn't use any FusionAuth features to store a user's departments and permissions. Instead, you can choose any naming scheme you want to represent your hierarchy. Here it's very important that you are able to map the text in the JSON with the names of your departments stored elsewhere. For instance, your permissions manager code would have to consistently use "Change Bank" and not "ChangeBank" for thousands of lines of JSON across hundreds of users.

If you are using the free edition of FusionAuth, using JSON might be suitable for you, but may become confusing. A better alternative would be to use FusionAuth only for user authentication, and keep all authorization and company structure information in a separate dedicated document management system that uses FusionAuth as its authentication gateway. Example document managements systems that can use an external OAuth provider like FusionAuth are [Nuxeo](https://doc.nuxeo.com/nxdoc/using-openid-oauth2-in-login-screen) and [M-Files](https://www.m-files.com/products/platform-security). (There may be other document managements systems that allow the use of FusionAuth, but their documentation does not state it.)
If you are using the free edition of FusionAuth, using JSON might be suitable for you, but may become confusing. A better alternative would be to use FusionAuth only for user authentication, and keep all authorization and company structure information in a separate dedicated document management system that uses FusionAuth as its authentication gateway. Example document management systems that can use an external OAuth provider like FusionAuth are [Nuxeo](https://doc.nuxeo.com/nxdoc/using-openid-oauth2-in-login-screen) and [M-Files](https://www.m-files.com/products/platform-security). (There may be other document management systems that allow the use of FusionAuth, but their documentation does not state it.)

Check failure on line 360 in astro/src/content/docs/extend/examples/modeling-hierarchies.mdx

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'Nuxeo'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'Nuxeo'?", "location": {"path": "astro/src/content/docs/extend/examples/modeling-hierarchies.mdx", "range": {"start": {"line": 360, "column": 453}}}, "severity": "ERROR"}

Check failure on line 360 in astro/src/content/docs/extend/examples/modeling-hierarchies.mdx

View workflow job for this annotation

GitHub Actions / runner / vale

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'Nuxeo'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'Nuxeo'?", "location": {"path": "astro/src/content/docs/extend/examples/modeling-hierarchies.mdx", "range": {"start": {"line": 360, "column": 453}}}, "severity": "ERROR"}
Loading