Defining Permissions
Define permission namespaces with JSDoc metadata for UI grouping and role suggestions
Defining Permissions
Omnibase allows you to define custom permission namespaces using TypeScript with JSDoc annotations. These annotations provide metadata for UI rendering and role suggestions.
Project Structure
Permissions are defined in your project's omnibase/permissions/ directory:
omnibase/
└── permissions/
├── types.ts # Type definitions
├── tenants.ts # Tenant namespace (required)
├── projects.ts # Project namespace (example)
└── ... # Your custom namespacesJSDoc Annotations
Each permission relation can have JSDoc annotations that provide metadata:
| Annotation | Description | Example |
|---|---|---|
@group | Primary grouping for UI | @group User Management |
@subGroup | Secondary grouping within a group | @subGroup Role Assignment |
@displayName | Human-readable name | @displayName Invite Users |
@role | Suggested default role (can have multiple) | @role owner |
@hidden | Hide from UI (internal relations) | @hidden |
Example with Annotations
import { Context, Namespace } from "./types";
export class User implements Namespace {}
export class ApiKey implements Namespace {}
export class Tenant implements Namespace {
related: {
/**
* @group User Management
* @displayName Invite Users
* @role owner
* @role admin
*/
can_invite_user: User[];
/**
* @group User Management
* @subGroup Role Assignment
* @displayName Update User Roles
* @role owner
* @role admin
*/
can_update_user_role: User[];
/**
* @group User Management
* @subGroup Role Assignment
* @displayName Promote to Owner
* @role owner
*/
can_update_user_role_to_owner: User[];
/**
* @group Database
* @subGroup Secrets
* @displayName View Database Password
* @role owner
* @role admin
*/
can_view_database_password: (User | ApiKey)[];
};
permits = {
invite_user: (ctx: Context): boolean =>
this.related.can_invite_user.includes(ctx.subject),
// ... other permits
};
}Hiding Internal Relations
Use the @hidden annotation for relations that are used internally for permission traversal but shouldn't appear in the UI:
export class Project implements Namespace {
related: {
/** @hidden */
parent_project: Project[];
/** @hidden */
tenant: Tenant[];
/**
* @group Configuration
* @displayName View Environment
* @role owner
* @role admin
*/
can_view_project_env: (User | ApiKey)[];
};
}Permission Types
Tenant Permissions (Auto-Scoped)
Tenant permissions are the only permissions that support auto-scoping. The active tenant ID is automatically injected when checking these permissions.
When you check a Tenant permission, Omnibase automatically uses the user's active tenant:
// Check if user can invite others to the current tenant
const result = await permissionsApi.checkPermission({
checkPermissionRequest: {
namespace: 'Tenant',
object: tenantId, // From session context
relation: 'can_invite_user',
subjectId: userId,
subjectNamespace: 'User',
},
});result, _, err := client.CheckPermission(ctx).CheckPermissionRequest(omnibase.CheckPermissionRequest{
Namespace: "Tenant",
Object: tenantId,
Relation: "can_invite_user",
SubjectSet: omnibase.SubjectSetRequest{
Namespace: "User",
Object: userId,
},
}).Execute()Resource Permissions (UUID Required)
Resource permissions do not support wildcards. You must always specify the resource UUID when creating or checking permissions.
For resources like projects, documents, or custom entities:
// Grant access to a specific project
await permissionsApi.createRelationship({
createRelationshipRequest: {
namespace: 'Project',
object: 'proj-123-uuid', // Specific UUID required
relation: 'can_write',
subjectId: userId,
subjectNamespace: 'User',
},
});_, _, err := client.CreateRelationship(ctx).CreateRelationshipRequest(omnibase.CreateRelationshipRequest{
Namespace: "Project",
Object: "proj-123-uuid",
Relation: "can_write",
SubjectSet: omnibase.SubjectSetRequest{
Namespace: "User",
Object: userId,
},
}).Execute()Hierarchical Permissions
Permissions can inherit from parent resources using the traverse() method:
export class Project implements Namespace {
related: {
/** @hidden */
parent_project: Project[];
/** @hidden */
tenant: Tenant[];
/**
* @group Database
* @subGroup Secrets
* @displayName View Database Password
* @role owner
* @role admin
*/
can_view_database_password: (User | ApiKey)[];
};
permits = {
view_database_password: (ctx: Context): boolean =>
// Check project-level permission
this.related.can_view_database_password.includes(ctx.subject) ||
// Check parent project (for nested projects)
this.related.parent_project.traverse((p) =>
p.permits.view_database_password(ctx)
) ||
// Fall back to tenant-level permission
this.related.tenant.traverse((t) =>
t.related.can_view_database_password.includes(ctx.subject)
),
};
}This allows granting permissions at the tenant level (applies to all projects) or project level (applies to specific projects).
Subject Types
Permissions can be granted to different subject types:
| Subject | Description | Usage |
|---|---|---|
User | Users with sessions | Standard user permissions |
ApiKey | Programmatic API keys | Service-to-service access |
Define which subjects can have a permission using TypeScript union types:
related: {
// User-only permissions
can_invite_user: User[];
can_delete_tenant: User[];
// Permissions for both Users and API keys
can_view_database_password: (User | ApiKey)[];
can_rotate_keys: (User | ApiKey)[];
}Syncing Permissions
After creating or updating permission files, sync them to Omnibase:
Validate Configuration
omnibase validate permissionsThis checks for syntax errors and invalid references.
Sync to Omnibase
omnibase sync permissionsThis uploads your permission namespaces with all JSDoc metadata.
Fetching Permission Definitions
The API returns all permission metadata including JSDoc annotations:
import { V1TenantsApi } from '@omnibase/core-js';
const tenantsApi = new V1TenantsApi(config);
const { data } = await tenantsApi.getRoleDefinitions({
subject: 'User', // or 'ApiKey'
});
for (const namespace of data.data.namespaces) {
console.log(`Namespace: ${namespace.name}`);
for (const relation of namespace.relations) {
console.log(` ${relation.name}`);
console.log(` Display: ${relation.displayName}`);
console.log(` Group: ${relation.group}`);
console.log(` SubGroup: ${relation.subGroup}`);
console.log(` Roles: ${relation.roles?.join(', ')}`);
}
}import omnibase "github.com/phoenixsolutionsgroup/omnibase/sdk/core/go"
tenantsClient := omnibase.NewAPIClient(&omnibase.Configuration{
Servers: omnibase.ServerConfigurations{
{URL: apiURL},
},
}).V1TenantsAPI
result, _, err := tenantsClient.GetRoleDefinitions(ctx).
Subject("User").
Execute()
for _, ns := range result.Data.Namespaces {
fmt.Printf("Namespace: %s\n", ns.Name)
for _, rel := range ns.Relations {
fmt.Printf(" %s\n", rel.Name)
fmt.Printf(" Display: %s\n", rel.DisplayName)
fmt.Printf(" Group: %s\n", rel.Group)
fmt.Printf(" SubGroup: %s\n", rel.SubGroup)
fmt.Printf(" Roles: %s\n", strings.Join(rel.Roles, ", "))
}
}Response Structure
interface RelationMetadata {
name: string; // e.g., "can_invite_user"
displayName?: string; // e.g., "Invite Users"
group?: string; // e.g., "User Management"
subGroup?: string; // e.g., "Role Assignment"
roles?: string[]; // e.g., ["owner", "admin"]
subjectTypes: string[]; // e.g., ["User"] or ["User", "ApiKey"]
}UI Components
Omnibase provides pre-built shadcn components that use the permission metadata:
Complete Example
import { Context, Namespace } from "./types";
export class User implements Namespace {}
export class ApiKey implements Namespace {}
export class Tenant implements Namespace {
related: {
/**
* @group Tenant Administration
* @displayName Delete Tenant
* @role owner
*/
can_delete_tenant: User[];
/**
* @group User Management
* @displayName Invite Users
* @role owner
* @role admin
*/
can_invite_user: User[];
/**
* @group User Management
* @displayName View Users
* @role owner
* @role admin
* @role dev
* @role member
*/
can_view_users: User[];
/**
* @group API Keys
* @displayName Create API Keys
* @role owner
* @role admin
* @role dev
*/
can_create_api_keys: User[];
/**
* @group Database
* @subGroup Secrets
* @displayName View Database Password
* @role owner
* @role admin
*/
can_view_database_password: (User | ApiKey)[];
};
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),
create_api_keys: (ctx: Context): boolean =>
this.related.can_create_api_keys.includes(ctx.subject),
view_database_password: (ctx: Context): boolean =>
this.related.can_view_database_password.includes(ctx.subject),
};
}import { ApiKey, Tenant, User } from "./tenants";
import { Context, Namespace } from "./types";
export class Project implements Namespace {
related: {
/** @hidden */
parent_project: Project[];
/** @hidden */
tenant: Tenant[];
/**
* @group Database
* @subGroup Secrets
* @displayName View Database Password
* @role owner
* @role admin
*/
can_view_database_password: (User | ApiKey)[];
/**
* @group Configuration
* @displayName Update Environment
* @role owner
* @role admin
* @role dev
*/
can_update_project_env: (User | ApiKey)[];
};
permits = {
view_database_password: (ctx: Context): boolean =>
this.related.can_view_database_password.includes(ctx.subject) ||
this.related.parent_project.traverse((p) =>
p.permits.view_database_password(ctx)
) ||
this.related.tenant.traverse((t) =>
t.related.can_view_database_password.includes(ctx.subject)
),
update_project_env: (ctx: Context): boolean =>
this.related.can_update_project_env.includes(ctx.subject) ||
this.related.parent_project.traverse((p) =>
p.permits.update_project_env(ctx)
) ||
this.related.tenant.traverse((t) =>
t.related.can_update_project_env.includes(ctx.subject)
),
};
}Example: Agent + Codebase
This example shows how to define custom objects and wire up permission traversal between them. You're building a platform where both users and LLM agents interact with codebases.
Scenario
User— built-in subject type for usersAgent— custom subject type for LLM agents (likeUser)Tenant— built-in organizational containerCodebase— custom resource object (git repository)
Both User and Agent can have permissions on a Codebase. Permissions can be granted directly on the codebase or at the tenant level (with traversal cascading down).
Define the Agent Namespace
import { Context, Namespace } from "./types";
export class Agent implements Namespace {}Define the Codebase Namespace
import { Agent, Context, Namespace, Tenant, User } from "./types";
export class Codebase implements Namespace {
related: {
/** @hidden */
tenant: Tenant[];
/** @hidden */
owner: User[];
/**
* @group Codebase Access
* @displayName Edit Code
* @role owner
* @role developer
*/
can_edit: (User | Agent)[];
/**
* @group Codebase Access
* @displayName Push Changes
* @role owner
* @role developer
*/
can_push: (User | Agent)[];
/**
* @group Codebase Access
* @displayName View Code
* @role owner
* @role developer
* @role viewer
*/
can_view: (User | Agent)[];
};
permits = {
edit: (ctx: Context): boolean =>
this.related.can_edit.includes(ctx.subject) ||
this.related.owner.includes(ctx.subject) ||
this.related.tenant.traverse((t) =>
t.related.can_edit.includes(ctx.subject)
),
push: (ctx: Context): boolean =>
this.related.can_push.includes(ctx.subject) ||
this.related.owner.includes(ctx.subject) ||
this.related.tenant.traverse((t) =>
t.related.can_push.includes(ctx.subject)
),
view: (ctx: Context): boolean =>
this.related.can_view.includes(ctx.subject) ||
this.related.owner.includes(ctx.subject) ||
this.related.tenant.traverse((t) =>
t.related.can_view.includes(ctx.subject)
),
};
}Relations Graph
┌──────────┐
│ User │
└────┬─────┘
│ can_edit
▼
┌──────────┐ tenant ┌──────────┐
│ Codebase │──────────────────▶│ Tenant │
└──────────┘ └──────────┘
▲ ▲
│ can_push │ can_edit
┌────┴─────┐ ┌───────┴──────┐
│ Agent │ │ User │
└──────────┘ └──────────────┘Granting Permissions
Give an agent push access to a specific codebase:
await permissionsApi.createRelationship({
createRelationshipRequest: {
namespace: 'Codebase',
object: 'cb-123',
relation: 'can_push',
subjectId: 'agent-456',
subjectNamespace: 'Agent',
},
});_, _, err := client.CreateRelationship(ctx).CreateRelationshipRequest(omnibase.CreateRelationshipRequest{
Namespace: "Codebase",
Object: "cb-123",
Relation: "can_push",
SubjectSet: omnibase.SubjectSetRequest{
Namespace: "Agent",
Object: "agent-456",
},
}).Execute()Now agent-456 can push to Codebase#cb-123.
Give a user push access to all codebases in a tenant. The traverse() call cascades this down:
await permissionsApi.createRelationship({
createRelationshipRequest: {
namespace: 'Tenant',
object: 'tenant-789',
relation: 'can_push',
subjectId: 'user-abc',
subjectNamespace: 'User',
},
});_, _, err := client.CreateRelationship(ctx).CreateRelationshipRequest(omnibase.CreateRelationshipRequest{
Namespace: "Tenant",
Object: "tenant-789",
Relation: "can_push",
SubjectSet: omnibase.SubjectSetRequest{
Namespace: "User",
Object: "user-abc",
},
}).Execute()Now user-abc can push to every codebase under Tenant#tenant-789, because permits.push traverses this.related.tenant as a fallback.
Set a user as the owner of a codebase. The traversal to owner handles the rest:
await permissionsApi.createRelationship({
createRelationshipRequest: {
namespace: 'Codebase',
object: 'cb-123',
relation: 'owner',
subjectId: 'user-abc',
subjectNamespace: 'User',
},
});_, _, err := client.CreateRelationship(ctx).CreateRelationshipRequest(omnibase.CreateRelationshipRequest{
Namespace: "Codebase",
Object: "cb-123",
Relation: "owner",
SubjectSet: omnibase.SubjectSetRequest{
Namespace: "User",
Object: "user-abc",
},
}).Execute()This creates the relation that owner.includes() uses in the permits block. Owners inherit edit, push, and view access without those needing to be granted separately.
Checking Permissions
Check if an agent can push to a codebase:
const { data } = await permissionsApi.checkPermission({
checkPermissionRequest: {
namespace: 'Codebase',
object: 'cb-123',
relation: 'push',
subjectId: 'agent-456',
subjectNamespace: 'Agent',
},
});
// data.allowed → true (from the direct grant above)result, _, err := client.CheckPermission(ctx).CheckPermissionRequest(omnibase.CheckPermissionRequest{
Namespace: "Codebase",
Object: "cb-123",
Relation: "push",
SubjectSet: omnibase.SubjectSetRequest{
Namespace: "Agent",
Object: "agent-456",
},
}).Execute()
// result.Data.Allowed → trueEvaluation path:
- Is
agent-456incan_pushofCodebase#cb-123? → Yes → ALLOWED
Check if a user can edit a codebase (granted at tenant level):
const { data } = await permissionsApi.checkPermission({
checkPermissionRequest: {
namespace: 'Codebase',
object: 'cb-123',
relation: 'edit',
subjectId: 'user-abc',
subjectNamespace: 'User',
},
});
// data.allowed → true (from the tenant-level grant above)result, _, err := client.CheckPermission(ctx).CheckPermissionRequest(omnibase.CheckPermissionRequest{
Namespace: "Codebase",
Object: "cb-123",
Relation: "edit",
SubjectSet: omnibase.SubjectSetRequest{
Namespace: "User",
Object: "user-abc",
},
}).Execute()
// result.Data.Allowed → trueEvaluation path:
- Is
user-abcincan_editofCodebase#cb-123? → No - Follow
ownerrelation → no owner set for this codebase - Follow
tenantrelation → isuser-abcincan_editofTenant#tenant-789? → Yes → ALLOWED
Creating Roles for Agent Permissions
Roles can include both User and Agent permissions. Create a "bot-admin" role that grants codebase permissions:
await tenantsApi.createRole({
createRoleRequest: {
roleName: 'bot-admin',
permissions: [
'codebase#can_push',
'codebase#can_edit',
'codebase#can_view',
],
},
});_, _, err := tenantsClient.CreateRole(ctx).CreateRoleRequest(omnibase.CreateRoleRequest{
RoleName: "bot-admin",
Permissions: []string{
"codebase#can_push",
"codebase#can_edit",
"codebase#can_view",
},
}).Execute()When you assign this role to a user, they can push/edit/view all codebases in the tenant (assuming the defines use traverse() to cascade from Tenant → Codebase).
Next Steps
- Role Configuration — Create roles from permission templates
- Permissions Concept — Deep dive into how permissions work
- Data Isolation — How permissions work with RLS