|
| 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 | + |
0 commit comments