Skip to content

Commit d53650b

Browse files
aaguiarzDaniel Yeamrhamzeh
authored
OpenFGA Adoption Patterns and Best Practices Section (#972)
Co-authored-by: Daniel Yeam <daniel.yeam@okta.com> Co-authored-by: Raghd Hamzeh <raghd.hamzeh@openfga.dev>
1 parent 22e4462 commit d53650b

File tree

2 files changed

+220
-0
lines changed

2 files changed

+220
-0
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
---
2+
title: Adoption Patterns
3+
sidebar_position: 3
4+
slug: /best-practices/adoption-patterns
5+
description: Describe different ways FGA can be adopted in an organization
6+
---
7+
8+
import {
9+
ProductName,
10+
ProductNameFormat,
11+
RelatedSection,
12+
13+
} from '@components/Docs';
14+
15+
# <ProductName format={ProductNameFormat.ShortForm}/> Adoption Patterns
16+
17+
This document outlines key implementation patterns for adopting <ProductName format={ProductNameFormat.ShortForm}/> in your organization.
18+
19+
## Starting with coarse-grained access control
20+
21+
When evaluating this solution, many companies start by replicating their existing permissions structure before moving to more granular controls. For example, if you're using Role-Based Access Control (RBAC) in a B2B scenario, you might start with a simple model:
22+
23+
```dsl.openfga
24+
model
25+
schema 1.1
26+
27+
type user
28+
type organization
29+
relations
30+
define admin : [user]
31+
define member : [user]
32+
# .. add additional organization roles
33+
34+
# map permissions to organization roles
35+
define can_add_member : admin
36+
define can_delete_member : admin
37+
define can_view_member : admin or member
38+
define can_add_resource : admin or member
39+
40+
```
41+
42+
You can define any number of roles for the organization type and then define the permissions based on those roles. You can then check if users have a specific permission at the organization level by calling the Check API on the organization object:
43+
44+
```
45+
Check(user: "user:anne", relation: "can_add_member", object: "organization:acme")
46+
```
47+
48+
A better implementation is to define the application's resource types in the model (e.g. documents, projects, insurance policies, bank accounts, etc):
49+
50+
51+
```dsl.openfga
52+
model
53+
schema 1.1
54+
55+
type user
56+
type organization
57+
relations
58+
define admin : [user]
59+
define member : [user]
60+
61+
define can_add_member : admin
62+
define can_delete_member : admin
63+
define can_view_member : admin or member
64+
define can_add_resource : admin or member
65+
66+
67+
type resource
68+
relations
69+
define organization : [organization]
70+
71+
# map resource permissions to organization roles
72+
define can_delete_resource : admin from organization or member from organization
73+
define can_view_resource : admin from organization or member from organization
74+
75+
```
76+
77+
In this case, you'll need to write tuples that establish the relationship between resource instances and organizations, or use Contextual Tuples to specify them, e.g:
78+
79+
```
80+
user: organization:acme
81+
relation: organization
82+
object: resource:root
83+
```
84+
85+
In this case, the Check() call will be at the resource level, for example:
86+
87+
```
88+
Check(user: "user:anne", relation: "can_view_resource", object: "resource:root")
89+
```
90+
91+
The main advantage of this approach is that your APIs will be checking permissions at the proper level. If you later want to evolve your authorization model to be more fine grained, you won't need to change your app. For example, you can add fine grained access permissions at the resource level, and your authorization check won't change:
92+
93+
```
94+
type resource
95+
relations
96+
define organization : [organization]
97+
define owner: [user]
98+
define viewer : [user]
99+
100+
# map resource permissions to organization roles
101+
define can_delete_resource : admin from organization or member from organization or owner
102+
define can_view_resource : admin from organization or member from organization or owner or viewer
103+
```
104+
105+
## Provide request-level data
106+
107+
One of the advantages of the Zanzibar/<ProductName format={ProductNameFormat.ShortForm}/> approach is that all the data you need to make authorization decisions is stored in a centralized database. That greatly simplifies how application implement access control. Applications do not need to retrieve al the required data before invoking an authorization service.
108+
109+
However, writing the data to the centralized store adds implementation complexity. You need to implement a data pipeline that makes sure the data is always up to date.
110+
111+
<ProductName format={ProductNameFormat.ShortForm}/> provides a feature called [Contextual Tuples](../interacting/contextual-tuples.mdx) that allows sending the required data as part of each authorization request instead of storing it on the <ProductName format={ProductNameFormat.ShortForm}/> database. Overusing this feature has many drawbacks, as you are now adding additional complexity and latency around collecting the data, and you are not benefiting from using <ProductName format={ProductNameFormat.ShortForm}/> as intended. However, implementing a hybrid approach can make sense in many scenarios and can also be a helpful tool at the start when you are transitioning into a more OpenFGA tailored approach.
112+
113+
When the data is already available to the calling API, sending it as a contextual tuple is very simple. A common use case is you have data in [your access tokens](../modeling/token-claims-contextual-tuples.mdx) (for example, roles/groups claims). Instead of synchronizing groups/roles relations to <ProductName format={ProductNameFormat.ShortForm}/>, you can send those as contextual tuples.
114+
115+
When the data is not already, you will need to retrieve it. This is what you need to do if you are implementing pure Attribute Access Control. You'd retrieve the data and send it to the authorization policy engine. You can do the same with <ProductName format={ProductNameFormat.ShortForm}/> using Contextual Tuples.
116+
117+
You'll need to make the trade-off between writing the data to <ProductName format={ProductNameFormat.ShortForm}/> so it's always available for any authorization request, or requesting it before making an authorization check.
118+
119+
We've seen companies successfully following a hybrid approach, starting by synchronizing the data that's easy first and providing the rest as contextual tuples. As their implementation matures, they implement more synchronization processes and stop sending the contextual tuples.
120+
121+
## Use <ProductName format={ProductNameFormat.ShortForm}/> to enrich JWTs
122+
123+
Once you have your authorization model and data set up, you can start making authorization checks from your application. The preferred way is to perform a [Check()](../getting-started/perform-check.mdx) call.
124+
125+
However, you might have a large set of APIs that are already making authorization checks using JWTs. Changing those applications can be a significant investment. Even if JWTs have several drawbacks compared to making FGA API calls, it can be reasonable to first start by using <ProductName format={ProductNameFormat.ShortForm}/> to generate the claims that are stored in JWTs, while the applications keep using those claims to make authorization decisions.
126+
127+
Over time, you'll migrate the applications and APIs to use authorization check instead.
128+
129+
Authentication services usually provide a way to enrich access tokens during the authorization flow. You can see an example on how to do it with Auth0 [here](https://auth0.com/blog/adding-custom-claims-to-id-token-with-auth0-actions/).
130+
131+
132+
For example, if you want to include in the access token the organizations that a user can log-in to, based on the following model:
133+
134+
```
135+
type user
136+
type organization
137+
relations
138+
define member : [user]
139+
```
140+
141+
You can call `ListObjects(type:"organization", relation:"member", user: "user:xxx")` and include those.
142+
143+
## Promoting Organization-Wide Adoption
144+
145+
To introduce <ProductName format={ProductNameFormat.ShortForm}/> in a large company, it's recommended that you identify a problem where the additional enables quickly delivering business value to customers. It can be a new project, a new module, a new feature. Using <ProductName format={ProductNameFormat.ShortForm}/> for such a project can be an easier decision. Once an implementation is successful, you can try influencing the rest of the organization to adopt it.
146+
147+
However, influencing the decision makers of a large organization can be hard. Each team has their own internal roadmaps and not all of the teams will see value in implementing a new authorization system. Migration can be seen as a tech-debt project instead of a business-value-driven one.
148+
149+
The can take advantage of the following capabilities to simplify adoption by multiple teams:
150+
151+
- [Modular Models](../modeling/modular-models.mdx) enable each team to independently evolve their authorization policies without relying on a central team.
152+
- [Access Control](../getting-started/setup-openfga/access-control.mdx) allows you to issue different credentials for each application, with permissions that ensure that each credential can only write data to the types defined in the Modules they own.
153+
154+
## Domain-Specific Authorization Server
155+
156+
Some companies decide to wrap <ProductName format={ProductNameFormat.ShortForm}/> with their own authorization service. They decide to do this for multiple reasons:
157+
158+
- Sometimes they already have a centralized service, and it's easy to replace it with another without changing the calling applications.
159+
- It can simplify internal adoption by providing domain-specific APIs. Instead of calling `write` or `check`, applications can call a `/share-document` endpoint or a `/can-view-document` one. Each team does not need to learn the <ProductName format={ProductNameFormat.ShortForm}/> API.
160+
- If they are using Contextual Tuples, they can keep the logic to retrieve additional data to send to <ProductName format={ProductNameFormat.ShortForm}/> in a single service.
161+
- They only need to provide <ProductName format={ProductNameFormat.ShortForm}/> configuration data like Store ID and Model ID in a single service.
162+
163+
On the other hand, adding another service increases latency, adds additional complexity and would make the teams less likely to find help from existing public OpenFGA documentation and resources.
164+
165+
## Shadowing the <ProductName format={ProductNameFormat.ShortForm}/> API
166+
167+
When migrating from an existing authorization system to <ProductName format={ProductNameFormat.ShortForm}/>, it's recommended to first run both systems in parallel, with <ProductName format={ProductNameFormat.ShortForm}/> in "shadow mode". This means that while the existing system continues to make the actual authorization decisions, you also make calls to <ProductName format={ProductNameFormat.ShortForm}/> asynchornously and compare the results.
168+
169+
This approach has several benefits:
170+
171+
- You can validate that your authorization model and relationship tuples are correctly configured before switching to <ProductName format={ProductNameFormat.ShortForm}/>.
172+
- You can measure the performance impact of adding <ProductName format={ProductNameFormat.ShortForm}/> calls to your application.
173+
- You can identify edge cases where the <ProductName format={ProductNameFormat.ShortForm}/> results differ from your existing system.
174+
- You can gradually build confidence in the <ProductName format={ProductNameFormat.ShortForm}/> implementation.
175+
176+
To implement shadow mode:
177+
178+
1. Configure your application to make authorization checks against both systems
179+
2. Log any discrepancies between the two systems
180+
3. Analyze the logs to identify and fix any issues
181+
4. Once confident in the results, switch to using <ProductName format={ProductNameFormat.ShortForm}/> as the source of truth. The same approach of shallow checks when [migrating between models](../getting-started/immutable-models.mdx#potential-use-cases).
182+
183+
This pattern is particularly useful for critical systems where authorization errors could have significant impact.
184+
185+
## Related Sections
186+
187+
<RelatedSection
188+
description="Check out these related resources for more information about adopting OpenFGA"
189+
relatedLinks={[
190+
{
191+
title: 'Production Best Practices',
192+
description: 'Learn about best practices for running OpenFGA in production environments.',
193+
link: './../getting-started/running-in-production',
194+
},
195+
{
196+
title: 'Modular Authorization Models',
197+
description: 'Learn how to break down your authorization model into modules.',
198+
link: './../modeling/modular-models',
199+
}
200+
]}
201+
/>
202+

docs/sidebars.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,24 @@ const sidebars = {
385385
},
386386
],
387387
},
388+
389+
{
390+
type: 'category',
391+
label: 'Best Practices',
392+
collapsible: true,
393+
collapsed: true,
394+
link: {
395+
type: 'doc',
396+
id: 'content/best-practices/adoption-patterns',
397+
},
398+
items: [
399+
{
400+
type: 'doc',
401+
label: 'Adoption Patterns',
402+
id: 'content/best-practices/adoption-patterns',
403+
}
404+
],
405+
},
388406
],
389407
};
390408

0 commit comments

Comments
 (0)