A comprehensive comparison of two modern authorization frameworks through a practical document management system example.
- Overview
- The Problem
- Quick Start
- Architecture Comparison
- Implementation Details
- Trade-offs Analysis
- When to Choose Each
This repository demonstrates two different approaches for implementing modern application authorization by comparing two open source tools: OpenFGA and Cedar.
We implement authorization for a multi-tenant document management system with organizations that own folders containing documents - a common real-world scenario that showcases the strengths and trade-offs of each approach.
We need authorization for a document management system with these requirements:
- Organization-based access: Users can view documents in their organization
- Ownership: Document/folder owners have full access (view, edit, delete, share)
- Explicit permissions: Grant editor/viewer permissions on documents and folders
- Inheritance: Folder permissions apply to contained documents
- âś… alice can view doc1: She's the owner
- âś… charlie can view doc2: Organization member + folder viewer permission
- ❌ david cannot view doc1: Different organization, no permissions
- âś… bob can view doc4: Explicit editor permission
- Go 1.19+
- Docker and Docker Compose
- curl (for setup scripts)
cd openfga/
./setup.sh
./openfga-check alice doc1 # âś… Owner access
./openfga-check david doc1 # ❌ Cross-organization deniedcd cedar/
./setup.sh
./cedar-check alice doc1 # âś… Owner access
./cedar-check david doc1 # ❌ Cross-organization deniedBoth examples provide identical authorization decisions using different approaches.
model
schema 1.1
type user
type organization
relations
define member: [user]
type folder
relations
# define parent: [folder] -> Not added because Cedar does not support recursion
define organization: [organization]
define owner: [user]
define editor: [user] or owner # or editor from parent
define viewer: [user] or editor or member from organization # or viewer from parent
define can_view: viewer
define can_edit: editor
define can_delete: owner
define can_share: owner or editor
type document
relations
define organization: [organization]
define parent_folder: [folder]
define owner: [user]
define editor: [user] or owner or editor from parent_folder
define viewer: [user] or editor or viewer from parent_folder or member from organization
define can_view: viewer
define can_edit: editor
define can_delete: owner
define can_share: owner or editor
Given that OpenFGA supports recursion, like inheriting folder's permissions, but Cedar does not, we won't be using recursion throughout this example.
In Cedar you can define an entity schema that you can use to validate policies, but is not a requirement. You can find the schema we use for this example here. You define the authorization policies in the Cedar language:
// Document Management Authorization Policies
// Organization member can view organization documents
permit(
principal,
action == DocumentManagement::Action::"ViewDocument",
resource
) when {
principal.organization == resource.organization
};
// Organization member can view organization folders
permit(
principal,
action == DocumentManagement::Action::"ViewFolder",
resource
) when {
principal.organization == resource.organization
};
// Document owner can perform all actions on their documents
permit(
principal,
action in [
DocumentManagement::Action::"ViewDocument",
DocumentManagement::Action::"EditDocument",
DocumentManagement::Action::"DeleteDocument",
DocumentManagement::Action::"ShareDocument"
],
resource
) when {
principal == resource.owner
};
// Document editor can edit and share documents they edit
permit(
principal,
action in [
DocumentManagement::Action::"EditDocument",
DocumentManagement::Action::"ShareDocument"
],
resource
) when {
principal in resource.editors
};
// Document viewer can view documents
permit(
principal,
action == DocumentManagement::Action::"ViewDocument",
resource
) when {
principal in resource.viewers
};
// Folder owner can perform all actions on their folders
permit(
principal,
action in [
DocumentManagement::Action::"ViewFolder",
DocumentManagement::Action::"EditFolder",
DocumentManagement::Action::"DeleteFolder",
DocumentManagement::Action::"ShareFolder"
],
resource
) when {
principal == resource.owner
};
// Folder editor can view, edit, and share folders (but not delete)
permit(
principal,
action in [
DocumentManagement::Action::"ViewFolder",
DocumentManagement::Action::"EditFolder",
DocumentManagement::Action::"ShareFolder"
],
resource
) when {
principal in resource.editors
};
// Folder editor can view, edit, and share documents in their folders
permit(
principal,
action in [
DocumentManagement::Action::"ViewDocument",
DocumentManagement::Action::"EditDocument",
DocumentManagement::Action::"ShareDocument"
],
resource
) when {
principal in resource.parent_folder.editors
};
// Folder owner can view, edit, and share documents in their folders
permit(
principal,
action in [
DocumentManagement::Action::"ViewDocument",
DocumentManagement::Action::"EditDocument",
DocumentManagement::Action::"ShareDocument"
],
resource
) when {
principal == resource.parent_folder.owner
};
// Folder viewers can view folders
permit(
principal,
action == DocumentManagement::Action::"ViewFolder",
resource
) when {
principal in resource.viewers
};
// Folder viewers can view documents in folders
permit(
principal,
action == DocumentManagement::Action::"ViewDocument",
resource
) when {
principal in resource.parent_folder.viewers
};
Both policies are equivalent and hopefully self-explanatory. The approaches are very different though. In OpenFGA permissions are defined in terms of relations, which lets you define all the different ways a user can get a permission in a single line (e.g. define viewer: [user] or editor or viewer from parent_folder or member from organization) while navigating resources hierarchies, and in Cedar you need to define define multiple permit clauses.
OpenFGA: Service-Based Authorization
- Runs as a separate service with its own database
- All authorization data stored in OpenFGA
- Single API call for authorization decisions
- Requires network roundtrip for each check
Cedar: Library-Based Authorization
- Runs as an embedded library in your application
- Data retrieved from your existing databases
- No network calls, but requires you to load the data
- Authorization logic coupled with data access
func checkAuthorization(fgaClient *client.OpenFgaClient, userID, documentID string) (bool, error) {
body := client.ClientCheckRequest{
User: fmt.Sprintf("user:%s", userID),
Relation: "can_view",
Object: fmt.Sprintf("document:%s", documentID),
}
data, err := fgaClient.Check(context.Background()).Body(body).Execute()
if err != nil {
return false, fmt.Errorf("check request failed: %w", err)
}
return *data.Allowed, nil
}// 1. Load data from your database
data, err := queryEntityData(db, userID, documentID)
if err != nil {
return false, err
}
// 2. Build Cedar entities from the data
entities := buildCedarEntities(data)
// 3. Create authorization request
request := cedar.Request{
Principal: cedar.NewEntityUID("User", userID),
Action: cedar.NewEntityUID("Action", "ViewDocument"),
Resource: cedar.NewEntityUID("Document", documentID),
}
// 4. Authorize with Cedar
decision, _ := cedar.Authorize(policySet, entities, request)
return decision == cedar.Allow, nilIn the Cedar example, we are using this SQL query to retrieve the data required to know if a user can view a document:
WITH user_org AS (
SELECT organization_id as user_org_id
FROM organization_members
WHERE user_id = $1
LIMIT 1
),
doc_info AS (
SELECT d.id as doc_id, d.organization_id as doc_org_id, d.folder_id, d.owner_id as doc_owner_id,
f.organization_id as folder_org_id, f.owner_id as folder_owner_id
FROM documents d
LEFT JOIN folders f ON d.folder_id = f.id
WHERE d.id = $2
),
doc_perms AS (
SELECT dp.document_id, dp.user_id, dp.permission_type, 'document' as resource_type
FROM document_permissions dp
WHERE dp.document_id = $2
),
folder_perms AS (
SELECT fp.folder_id as document_id, fp.user_id, fp.permission_type, 'folder' as resource_type
FROM folder_permissions fp
JOIN doc_info di ON fp.folder_id = di.folder_id
WHERE di.folder_id IS NOT NULL
)
SELECT
uo.user_org_id, di.doc_id, di.doc_org_id, di.folder_id, di.doc_owner_id, di.folder_org_id, di.folder_owner_id,
COALESCE(dp.user_id, '') as perm_user_id,
COALESCE(dp.permission_type, '') as perm_type,
COALESCE(dp.resource_type, '') as resource_type
FROM user_org uo
CROSS JOIN doc_info di
LEFT JOIN (
SELECT * FROM doc_perms
UNION ALL
SELECT * FROM folder_perms
) dp ON trueThere are other ways to write a single or multiple queries and get a similar results. After you retrieve the data, you need to convert it to an instance of a Cedar Entity. The cedar/main.go program has the full example.
In general, when using OpenFGA, you will store all the data required to make authorization decisions in OpenFGA. When using Cedar, you'll store it in your application.
However, OpenFGA allows a hybrid model, where you can actually specify the data required to make the decision in Contextual Tuples. Conceptually, you can do something equivalent to what the Cedar example shows, get all the data from a SQL database, and send it as part of the authorization request.
It would not make sense to use OpenFGA that way, though. If in all scenarios you are going to first retrieve the data from your database, Cedar is a better option.
On the other hand, combining having data in OpenFGA AND sending contextual data gives you a lot of flexibility. If you can easily synchronize data to OpenFGA, you'd do that. When you can't, because data is not stored in a database (e.g. the content of an access token), or because synchronizing it is hard, you can send it as part of the request.
| Aspect | OpenFGA | Cedar |
|---|---|---|
| Latency | Network call required, but optimized for relationship queries | No network call, but requires data loading |
| Complexity | Simple API calls, easy integration | Complex data loading and entity building |
| Maintainability | Policy changes don't affect app code | Policy changes may require SQL changes |
| Operations | Requires running separate service + database | No additional infrastructure |
| List Operations | Native "list all documents user can view" | Requires custom SQL, post-filtering or experimental partial evaluation |
| Data Consistency | Dual-write problem for data sync | Uses existing transactional data |
| Recursion | Does support modeling recursive permissions | It does not support recursive permissions |
- OpenFGA: Network roundtrip required, but queries are optimized and cacheable
- Cedar: No network call, but data loading latency depends on query complexity
- OpenFGA: Simple API calls - easily integrated into API gateways
- Cedar: Requires data retrieval and transformation - more complex integration
- OpenFGA: Policy changes isolated from application code
- Cedar: Authorization logic coupled with database queries
- OpenFGA: Additional service to operate, but dedicated authorization infrastructure
- Cedar: No extra infrastructure, but higher database load
- OpenFGA: Built-in ListObjects and ListUsers APIs. The latency for those calls will heavily depend on the authorization model.
- Cedar: Requires encoding authorization logic in SQL, post-filtering results, or use the experimental partial evaluation implementation to generate a filter for your local database.
Given the differences in architecture, a performance comparison between both engines does not make sense:
- The raw Authorize call from Cedar will always be much faster than the equivalent OpenFGA operation, as it does not require a network call.
- The overall performance will depend on how each system retrieves the data required to make the decision. OpenFGA is designed to optimize how traverse the data. Data management is out of scope for Cedar.
- You need fine-grained permissions with complex inheritance
- List operations are important ("show all documents user can view")
- You want authorization data logic separate from business logic
- You require additional data when additional data when making authorization decisions
- Your authorization requirements are relationship based rather than attribute-based
- You have rich entity attributes that drive decisions
- You want to minimize infrastructure complexity
- Your authorization is primarily attribute-based rather than relationship-based
- The application already has all the data required to make authorization decisions
- OpenFGA Documentation
- OpenFGA Go SDK
- Zanzibar Paper - The original Google paper
- OpenFGA Playground - Interactive modeling tool
See CONTRIBUTING.
This project is licensed under the Apache-2.0 license. See the LICENSE file for more info.