Omnibase

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 namespaces

JSDoc Annotations

Each permission relation can have JSDoc annotations that provide metadata:

AnnotationDescriptionExample
@groupPrimary grouping for UI@group User Management
@subGroupSecondary grouping within a group@subGroup Role Assignment
@displayNameHuman-readable name@displayName Invite Users
@roleSuggested default role (can have multiple)@role owner
@hiddenHide from UI (internal relations)@hidden

Example with Annotations

omnibase/permissions/tenants.ts
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:

omnibase/permissions/projects.ts
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:

omnibase/permissions/projects.ts
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:

SubjectDescriptionUsage
UserUsers with sessionsStandard user permissions
ApiKeyProgrammatic API keysService-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 permissions

This checks for syntax errors and invalid references.

Sync to Omnibase

omnibase sync permissions

This uploads your permission namespaces with all JSDoc metadata.

Verify

omnibase permissions list

Lists all registered namespaces with their relations and 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

omnibase/permissions/tenants.ts
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),
  };
}
omnibase/permissions/projects.ts
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 users
  • Agent — custom subject type for LLM agents (like User)
  • Tenant — built-in organizational container
  • Codebase — 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

omnibase/permissions/agents.ts
import { Context, Namespace } from "./types";

export class Agent implements Namespace {}

Define the Codebase Namespace

omnibase/permissions/codebases.ts
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 → true

Evaluation path:

  1. Is agent-456 in can_push of Codebase#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 → true

Evaluation path:

  1. Is user-abc in can_edit of Codebase#cb-123? → No
  2. Follow owner relation → no owner set for this codebase
  3. Follow tenant relation → is user-abc in can_edit of Tenant#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 TenantCodebase).


Next Steps

On this page