Skip to main content

Overview

Create custom authorization models in OpenFGA to match your application’s permission requirements. This guide covers model design, testing, and best practices.

Authorization Model Language

OpenFGA models are defined in a simple DSL:
model
  schema 1.1

type user

type organization
  relations
    define admin: [user]
    define member: [user] or admin

Key Concepts

Types

Objects in your system (user, organization, document)

Relations

Relationships between objects (admin, member, viewer)

Conditions

Context-based permissions (time, IP, attributes)

Default Model

The MCP Server includes this model:
model
  schema 1.1

type user

type role
  relations
    define assignee: [user]

type organization
  relations
    define admin: [user]
    define member: [user] or admin
    define viewer: [user] or member

type tool
  relations
    define owner: [user]
    define executor: [user, organization#member]
    define viewer: [user, organization#member]

type conversation
  relations
    define owner: [user]
    define editor: [user]
    define viewer: [user] or editor or owner

type document
  relations
    define owner: [user]
    define editor: [user]
    define viewer: [user] or editor or owner
Supported Patterns:
  • Hierarchical roles (admin → member → viewer)
  • Organization-based access (organization#member)
  • Resource ownership
  • Delegated permissions

Custom Models

Example: Project Management

model
  schema 1.1

type user

type team
  relations
    define lead: [user]
    define member: [user] or lead

type project
  relations
    define owner: [team#lead]
    define contributor: [team#member]
    define viewer: [user] or contributor

type task
  relations
    define project: [project]
    define assignee: [user]
    define viewer: [user] from project
Usage:
## Team lead creates project
await client.write_tuples([
    {"user": "user:alice", "relation": "lead", "object": "team:engineering"},
    {"user": "team:engineering#lead", "relation": "owner", "object": "project:webapp"}
])

## Alice can view project (via team lead)
allowed = await client.check_permission(
    user="user:alice",
    relation="viewer",
    object="project:webapp"
)
## Returns: True

Example: Multi-Tenant SaaS

model
  schema 1.1

type user

type tenant
  relations
    define admin: [user]
    define member: [user] or admin

type workspace
  relations
    define tenant: [tenant]
    define admin: [user, tenant#admin]
    define member: [user, tenant#member] or admin

type resource
  relations
    define workspace: [workspace]
    define owner: [user]
    define editor: [user, workspace#member]
    define viewer: [user] or editor
Isolation:
## Tenant A admin
await client.write_tuples([
    {"user": "user:alice", "relation": "admin", "object": "tenant:company_a"},
    {"user": "tenant:company_a", "relation": "tenant", "object": "workspace:ws1"}
])

## Alice can access workspace (via tenant admin)
allowed = await client.check_permission(
    user="user:alice",
    relation="admin",
    object="workspace:ws1"
)
## Returns: True

Example: Content Management

model
  schema 1.1

type user

type folder
  relations
    define parent: [folder]
    define owner: [user]
    define editor: [user]
    define viewer: [user] or editor or owner or viewer from parent

type file
  relations
    define parent: [folder]
    define owner: [user]
    define editor: [user]
    define viewer: [user] or editor or owner or viewer from parent
Inheritance:
## Set folder permissions
await client.write_tuples([
    {"user": "user:alice", "relation": "owner", "object": "folder:docs"},
    {"user": "folder:docs", "relation": "parent", "object": "file:report.pdf"}
])

## Alice can view file (inherited from folder)
allowed = await client.check_permission(
    user="user:alice",
    relation="viewer",
    object="file:report.pdf"
)
## Returns: True

Relation Operators

Direct Assignment

define admin: [user]
Users directly assigned admin relation.

Union (OR)

define member: [user] or admin
User is member OR admin.

Intersection (AND)

define can_delete: admin and owner
User must be admin AND owner.

Exclusion (BUT NOT)

define active_member: member but not suspended
User is member but not suspended.

Computed Relations

define viewer: [user] or viewer from parent
User is viewer OR inherits from parent.

Conditional Permissions

Add context-based rules:
model
  schema 1.1

type user

type document
  relations
    define owner: [user]
    define viewer: [user with time_limited_access]

condition time_limited_access(current_time: timestamp, expiry: timestamp) {
  current_time < expiry
}
Usage:
## Check with context
allowed = await client.check_permission(
    user="user:bob",
    relation="viewer",
    object="document:report",
    context={
        "current_time": "2025-10-12T10:00:00Z",
        "expiry": "2025-10-13T10:00:00Z"
    }
)

Testing Models

Model Validation

## Use OpenFGA CLI
fga model validate --file model.fga

## Or via API
curl -X POST http://localhost:8080/stores/{store_id}/authorization-models \
  -d @model.json

Test Assertions

test_cases = [
    {
        "user": "user:alice",
        "relation": "admin",
        "object": "organization:acme",
        "expected": True
    },
    {
        "user": "user:bob",
        "relation": "admin",
        "object": "organization:acme",
        "expected": False
    }
]

for test in test_cases:
    result = await client.check_permission(
        user=test["user"],
        relation=test["relation"],
        object=test["object"]
    )
    assert result == test["expected"], f"Test failed: {test}"

Deploying Models

Via Setup Script

Update scripts/setup/setup_openfga.py:
AUTHORIZATION_MODEL = {
    "schema_version": "1.1",
    "type_definitions": [
        {
            "type": "user"
        },
        {
            "type": "organization",
            "relations": {
                "admin": {"this": {}},
                "member": {
                    "union": {
                        "child": [
                            {"this": {}},
                            {"computedUserset": {"relation": "admin"}}
                        ]
                    }
                }
            }
        }
    ]
}

## Write model
response = await client.write_authorization_model(AUTHORIZATION_MODEL)
model_id = response["authorization_model_id"]

Via API

curl -X POST http://localhost:8080/stores/{store_id}/authorization-models \
  -H "Content-Type: application/json" \
  -d '{
    "schema_version": "1.1",
    "type_definitions": [...]
  }'

Best Practices

Begin with basic types and relations. Add complexity as needed.Good:
type document
  relations
    define owner: [user]
    define viewer: [user]
Too Complex:
type document
  relations
    define owner: [user]
    define editor: [user, group#member, team#lead]
    define viewer: [user] or editor or viewer from parent or viewer from workspace
Define role hierarchies to reduce tuple count.
type organization
  relations
    define admin: [user]
    define member: [user] or admin  # Admins are members
    define viewer: [user] or member # Members are viewers
Too many hops slow down checks.Good (2 hops):
define viewer: [user] or viewer from parent
Bad (5+ hops):
define viewer: [user] or viewer from parent from grandparent from workspace from tenant
Use consistent naming across your model.
  • Types: Singular (user, document, not users)
  • Relations: Lowercase (admin, not Admin)
  • IDs: Prefixed (user:123, not 123)
Track model changes and test migrations.
# v1 model
model_v1 = {...}

# v2 model (add new type)
model_v2 = {...}

# Test migration
# 1. Create v2 model
# 2. Verify v1 tuples still work
# 3. Add v2 tuples
# 4. Switch to v2

Performance Optimization

Reduce Tuple Count

Use group/organization relations instead of per-user tuples: Bad (100 tuples for 100 users):
for user_id in range(100):
    await client.write_tuples([{
        "user": f"user:{user_id}",
        "relation": "viewer",
        "object": "document:report"
    }])
Good (1 tuple):
await client.write_tuples([{
    "user": "organization:company#member",
    "relation": "viewer",
    "object": "document:report"
}])

Add Indexes

For PostgreSQL backend, add indexes on frequently queried fields.

Enable Caching

client = OpenFGAClient(
    cache_ttl=300,  # 5 minutes
    cache_backend="redis"
)

Migration Guide

Adding New Types

  1. Create new model with added type
  2. Write new model to OpenFGA
  3. No data migration needed

Modifying Relations

  1. Create new model
  2. Write new model
  3. Migrate existing tuples if needed
## Old: "can_read" relation
old_tuples = await client.read_tuples(relation="can_read")

## New: "viewer" relation
new_tuples = [{
    "user": t["user"],
    "relation": "viewer",  # Changed
    "object": t["object"]
} for t in old_tuples]

await client.write_tuples(new_tuples)
await client.delete_tuples(old_tuples)

Next Steps


Flexible Authorization: Custom permission models support any access control pattern!