Permissions
Fine-grained permissions with role-based grouping
How Permissions Work
OmniBase uses Ory Keto for permission management. The system is built around fine-grained permissions that are grouped into roles for easier management.
Core Concepts
Fine-Grained Permissions
Permissions are atomic actions a user can perform. Each permission is a specific capability:
can_invite_user
can_delete_tenant
can_view_users
can_view_database_password
can_update_project_envPermissions are defined within namespaces (like Tenant or Project). In roles.config.json, tenant permissions use the format tenant#permission_name.
Roles Group Permissions
Roles are collections of permissions defined in roles.config.json:
{
"roles": [
{
"role": "owner",
"permissions": [
"tenant#can_delete_tenant",
"tenant#can_invite_user",
"tenant#can_remove_user",
"tenant#can_update_user_role",
"tenant#can_view_users",
"tenant#can_create_api_keys",
"tenant#can_view_api_keys",
"tenant#can_revoke_api_keys"
]
},
{
"role": "admin",
"permissions": [
"tenant#can_invite_user",
"tenant#can_remove_user",
"tenant#can_update_user_role",
"tenant#can_view_users",
"tenant#can_create_api_keys",
"tenant#can_view_api_keys"
]
},
{
"role": "member",
"permissions": [
"tenant#can_view_users"
]
}
]
}When you assign a user the admin role, they receive all permissions in that role's array.
Defining Permissions
Permissions are defined using Ory Permission Language (OPL) in TypeScript namespace files:
// permissions/tenants.ts
class Tenant implements Namespace {
related: {
can_delete_tenant: User[];
can_invite_user: User[];
can_remove_user: User[];
can_view_users: User[];
can_create_api_keys: User[];
can_view_api_keys: User[];
can_revoke_api_keys: User[];
};
permits = {
delete_tenant: (ctx: Context): boolean =>
this.related.can_delete_tenant.includes(ctx.subject),
invite_user: (ctx: Context): boolean =>
this.related.can_invite_user.includes(ctx.subject),
view_users: (ctx: Context): boolean =>
this.related.can_view_users.includes(ctx.subject),
};
}Each related field defines a permission. The permits object defines how those permissions are checked.
Permission Inheritance
Permissions can inherit from parent resources. For example, project permissions can fall back to tenant-level permissions:
// permissions/projects.ts
class Project implements Namespace {
related: {
tenant: Tenant[];
can_view_database_password: (User | ApiKey)[];
can_update_project_env: (User | ApiKey)[];
};
permits = {
view_database_password: (ctx: Context): boolean =>
// Check project-level permission first
this.related.can_view_database_password.includes(ctx.subject) ||
// Fall back to tenant-level permission
this.related.tenant.traverse((t) =>
t.related.can_view_database_password.includes(ctx.subject)
),
};
}This allows you to grant permissions at either the tenant level (applies to all projects) or the project level (applies to specific projects).
How Checks Work
When checking if a user can perform an action:
Can user:alice delete tenant:acme-corp?
│
├─▶ Look up alice's role in acme-corp → "admin"
│
├─▶ Check if "admin" role includes "tenant#can_delete_tenant"
│ └─▶ No → DENIED
│
└─▶ Result: NOT ALLOWEDFor an owner:
Can user:bob delete tenant:acme-corp?
│
├─▶ Look up bob's role in acme-corp → "owner"
│
├─▶ Check if "owner" role includes "tenant#can_delete_tenant"
│ └─▶ Yes → Check relation tuple exists
│ └─▶ (Tenant, acme-corp, can_delete_tenant, user:bob) exists
│
└─▶ Result: ALLOWEDTenant vs Resource Permissions
All permission checks require a namespace, object ID, relation, and subject. The difference is how the object ID is obtained:
Tenant Permissions
For tenant permissions, the RBAC middleware automatically retrieves the tenant ID from the user's session context:
// Middleware handles getting tenant ID from session
tenantID := ctx.GetString("tenant_id")
checkPermission(ctx, "Tenant", tenantID, relation)In roles.config.json, tenant permissions use the tenant# prefix:
"permissions": ["tenant#can_invite_user", "tenant#can_delete_tenant"]Resource Permissions (e.g., Project)
For resource permissions like Project, the object ID must come from the request (e.g., URL parameter):
// Project ID comes from URL param
projectID := ctx.Param("project_id")
checkPermission(ctx, "Project", projectID, relation)Project permissions inherit from tenant-level permissions, so if a user has a permission at the tenant level, they have it for all projects in that tenant.
API Keys
API keys can be granted the same permissions as users. This requires two things:
1. Define an Empty ApiKey Namespace
The ApiKey namespace is defined as an empty class - it exists only as a subject identifier:
// permissions/tenants.ts
export class ApiKey implements Namespace {}2. Allow ApiKey in Permission Definitions
For permissions that API keys should be able to have, include ApiKey in the type union:
related: {
// User-only permissions
can_invite_user: User[];
can_delete_tenant: User[];
// Permissions that can be granted to Users OR API keys
can_view_database_password: (User | ApiKey)[];
can_rotate_keys: (User | ApiKey)[];
}Checking API Key Permissions
When checking permissions for an API key, you must specify the ApiKey namespace as the subject:
// Subject for a user
SubjectSet{Namespace: "User", Object: userId}
// Subject for an API key
SubjectSet{Namespace: "ApiKey", Object: apiKeyId}The middleware determines which subject type to use based on the authentication method:
switch authMethod {
case "session":
namespace = "User"
object = ctx.GetString("user_id")
case "api_key":
namespace = "ApiKey"
object = ctx.GetString("api_key")
}Custom Roles
You can define custom roles beyond the defaults:
{
"role": "billing_admin",
"permissions": [
"tenant#can_view_billing",
"tenant#can_update_billing",
"tenant#can_view_invoices"
]
}Summary
- Define fine-grained permissions in namespace files using OPL
- Group permissions into roles in
roles.config.json - Assign users to roles - they receive all permissions in that role
- Check permissions at runtime:
- Tenant permissions use the active tenant context automatically
- Resource permissions (Project, etc.) require explicit object IDs
This approach gives you the flexibility of fine-grained access control with the simplicity of role-based management.