Permission Model (OpenFGA & Tuples)
Easy AppServer uses OpenFGA for fine-grained, relation-based authorization that scales from simple checks to complex hierarchies.
Overview
The permission model is based on relationship tuples that express who can do what to which resources. This approach, inspired by Google Zanzibar, enables flexible access control that adapts to your application's needs.
Core Concepts
Tuples
A tuple is a simple statement of relationship:
(user, relation, object)
Examples:
- (user:alice, viewer, app:todos)
- (role:admin#assignee, owner, route:/api/todos)
- (app:backend, trigger, hook:user.created)
Relations
Relations define the type of access or relationship:
Common Relations:
owner- Full control over the resourceeditor- Can modify but not deleteviewer- Read-only accessaccessible_by- Can access a routegranted_to- Has a specific permissiontrigger- Can trigger a hookexecute- Can execute an activity
Objects
Objects are the resources being protected:
- app:de.easy-m.todos
- route:/api/todos
- hook:user.created
- activity:send-email
- setting:api-key
Tuple Structure
User-to-Role Assignment
Tuple: (user:alice, assignee, role:admin)
Meaning: Alice is assigned the admin role
Role-to-Resource Permission
Tuple: (role:admin#assignee, owner, app:todos)
Meaning: Users with the admin role are owners of the todos app
Note: The #assignee syntax means "users who have assignee relation to role:admin"
App-to-Permission Grant
Tuple: (app:backend, trigger, hook:user.created)
Meaning: The backend app can trigger the user.created hook
Permission Scopes
Different resource types use different tuple patterns:
Route Permissions
Tuple: (role:advertiser#assignee, accessible_by, route:/api/campaigns)
Check: Can user:bob access route:/api/campaigns?
Resolved via: bob → role:advertiser → accessible_by → route
Code Reference: pkg/v2/application/proxy/ - HTTP proxy verifies permissions before forwarding
Hook Permissions
Trigger Tuple: (app:backend, trigger, hook:user.created)
Listen Tuple: (app:notifier, listen, hook:user.created)
Actions:
- trigger: Can publish the hook
- listen: Can subscribe to hook events
Code Reference: pkg/v2/application/hooks/hooks_service.go:16
Activity Permissions
Tuple: (app:worker, execute, activity:generate-report)
Check: Can app:worker execute activity:generate-report?
Code Reference: pkg/v2/application/activity/activity_service.go:12
Settings Permissions
Tuple: (role:admin#assignee, write, setting:api-key)
Tuple: (role:viewer#assignee, read, setting:api-key)
Granular control over configuration access
Permission Checker Flow
The platform uses a multi-step process to check permissions:
Code Reference: pkg/v2/infrastructure/authz/permission_checker.go:10
1. Authentication
Identify the requester:
- User Auth: Session cookie → Kratos → User ID
- App Auth: Certificate → Signature verification → App name
2. Cache Lookup
Check multi-level cache (fastest → slowest):
1. In-memory cache (local to appserver instance)
2. Redis cache (shared across instances)
3. OpenFGA API (authoritative source)
Code Reference: pkg/v2/infrastructure/permission/cache/cache.go:12
3. OpenFGA Query
If not cached, query OpenFGA:
POST /stores/{store_id}/check
{
"tuple_key": {
"user": "user:alice",
"relation": "viewer",
"object": "app:todos"
}
}
Response: { "allowed": true }
4. Cache Update
Store result in all cache layers with TTL.
5. Decision
Return allow/deny decision to caller.
Tuple Population
Tuples are created automatically during app installation:
Code Reference: pkg/v2/application/marketplace/marketplace_service.go:43
Installation Flow
1. App manifest declares required permissions
2. Marketplace service parses permissions
3. For each permission, create tuple:
Tuple: (app:name, action, resource)
4. Write tuples to OpenFGA
Uninstallation Flow
1. Query all tuples for app:name
2. Delete tuples from OpenFGA
3. Publish a `permission.invalidate` event so caches drop the affected tuples
Caching Strategy
The permission system uses aggressive caching to minimize latency:
Cache Layers
L1: In-Memory Cache (per appserver instance)
- TTL: None (entries live until invalidated)
- Eviction: Manual only
- Latency: < 1ms
L2: Redis Cache (shared)
- TTL: Configurable (default 5 minutes)
- Latency: 2-5ms
L3: OpenFGA (source of truth)
- No TTL (always fresh)
- Unlimited size
- Latency: 10-50ms
Code Reference: pkg/v2/infrastructure/permission/cache/cache.go:12
Cache Invalidation
Caches are invalidated on:
- Tuple creation/deletion
- Role assignments changed
- App installation/uninstallation
- Explicit cache clear command
Invalidation Signals
Event Bus: permission.invalidate
Payload: { app_name, pattern }
Handler:
1. Remove from in-memory cache
2. Remove from Redis cache
3. Broadcast to other appserver instances
Permission Declaration in Manifest
Apps declare permissions in their manifest:
defineManifest()
.name('de.easy-m.backend')
.permission('hook', 'user.created', 'trigger')
.permission('hook', 'user.deleted', 'listen')
.permission('route', '/api/users', 'read')
.permission('activity', 'send-email', 'execute')
.build();
This generates tuples:
(app:de.easy-m.backend, trigger, hook:user.created)
(app:de.easy-m.backend, listen, hook:user.deleted)
(app:de.easy-m.backend, read, route:/api/users)
(app:de.easy-m.backend, execute, activity:send-email)
Role Hierarchy
The platform defines 4 base roles with inheritance:
easy (god mode)
↓
admin
↓
advertiser / publisher (parallel)
Role Relations
role:easy inherits from role:admin
role:admin inherits from role:advertiser
role:admin inherits from role:publisher
Example Checks
Query: Can user:alice (admin) view app:todos?
Resolution:
1. alice → assignee → role:admin
2. role:admin inherits from role:advertiser
3. role:advertiser#assignee → viewer → app:todos
4. Result: Yes (via inheritance)
Code Reference: docker/scripts/setup-openfga-roles.sh - Role initialization
OpenFGA Authorization Model
The authorization model defines valid relationships:
model
schema 1.1
type user
type role
relations
define assignee: [user]
type app
relations
define owner: [role#assignee]
define editor: [role#assignee] or owner
define viewer: [role#assignee] or editor
type route
relations
define accessible_by: [role#assignee, app]
type hook
relations
define trigger: [app]
define listen: [app]
type activity
relations
define execute: [app]
Code Reference: docker/openfga/authorization-model.json
Performance Optimization
Batch Checks
For multiple permission checks:
POST /stores/{store_id}/batch-check
{
"checks": [
{"user": "user:alice", "relation": "viewer", "object": "app:todos"},
{"user": "user:alice", "relation": "viewer", "object": "app:notes"}
]
}
Reduces round trips from N to 1.
Permission Hints
Pre-populate caches based on user roles:
On user login:
1. Query all permissions for user's roles
2. Warm up cache with likely checks
3. Subsequent checks hit cache
Stats Tracking
The cache tracks hit rates:
Cache Stats:
- L1 Hits: 95% (excellent)
- L2 Hits: 4%
- L3 Hits (OpenFGA): 1%
Total latency: ~1ms avg
Security Considerations
Tuple Immutability
Once created, tuples should be immutable. To change permissions:
- Delete old tuple
- Create new tuple
- Invalidate caches
Audit Logging
All permission checks and tuple changes are logged:
Log: permission.checked
Data: { user, action, resource, allowed, latency, cache_hit }
Log: tuple.created
Data: { tuple, created_by, timestamp }
Log: tuple.deleted
Data: { tuple, deleted_by, timestamp }
Least Privilege
Apps should request minimum necessary permissions:
❌ Permission: ("app", "owner", "route:/*") // Too broad
✓ Permission: ("app", "read", "route:/api/todos") // Specific
Troubleshooting Permissions
Check Tuple Existence
# Via OpenFGA CLI
fga tuple list --store-id=${STORE_ID}
# Via API
curl http://localhost:8090/stores/${STORE_ID}/read
Verify Cache State
# Redis
redis-cli KEYS "permission:*"
# Get specific entry
redis-cli GET "permission:user:alice:viewer:app:todos"
Debug Permission Denial
1. Check authentication (is user/app identified?)
2. Query OpenFGA directly (bypass cache)
3. Verify tuple exists
4. Check role assignments
5. Review inheritance chain
Related Concepts
- Authentication & Authorization - Authentication flow before permission checks
- Hooks Architecture - Hook permission enforcement
- Activities & Background Workflows - Activity permission enforcement
- Caching Strategy - Permission caching details
- Application Manifest - Declaring permissions
Further Reading
- OpenFGA Documentation - Authorization model details
- Getting Started: OpenFGA Setup - Initial configuration