Omnibase

Custom Permissions

Define permission namespaces and relations for your application

Custom Permissions

OmniBase allows you to define custom permission namespaces and relations that match your application's domain model. Permissions are defined in TypeScript files and synced via the CLI.

UI Components

OmniBase provides pre-built shadcn components for permission management in @omnibase/shadcn:

Project Structure

Custom permissions are defined in your project's omnibase/permissions/ directory:

omnibase/
└── permissions/
    ├── roles.config.json    # Role definitions
    ├── tenants.ts           # Tenant namespace (required)
    ├── projects.ts          # Project namespace (example)
    ├── documents.ts         # Document namespace (example)
    └── ...                  # Your custom namespaces

Defining Permission Namespaces

Permission namespaces define the entities in your system and the relations users can have with them.

Basic Namespace Structure

omnibase/permissions/projects.ts
import type { Namespace, Context } from "@ory/keto-namespace-types";

class User implements Namespace {}

class Project implements Namespace {
  // Define the relations that can exist
  related: {
    owners: User[];
    editors: User[];
    viewers: User[];
  };

  // Define computed permissions
  permits = {
    // Only owners can delete
    delete: (ctx: Context) =>
      this.related.owners.includes(ctx.subject),

    // Owners and editors can write
    write: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.editors.includes(ctx.subject),

    // All relations can read
    read: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.editors.includes(ctx.subject) ||
      this.related.viewers.includes(ctx.subject),
  };
}

Tenant Namespace (Required)

The tenant namespace is required and defines tenant-level permissions:

omnibase/permissions/tenants.ts
import type { Namespace, Context } from "@ory/keto-namespace-types";

class User implements Namespace {}

class Tenant implements Namespace {
  related: {
    owners: User[];
    admins: User[];
    members: User[];
  };

  permits = {
    // Only owners can delete the tenant
    delete_tenant: (ctx: Context) =>
      this.related.owners.includes(ctx.subject),

    // Owners and admins can manage members
    invite_user: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.admins.includes(ctx.subject),

    remove_user: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.admins.includes(ctx.subject),

    update_user_role: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.admins.includes(ctx.subject),

    // All members can view the user list
    view_users: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.admins.includes(ctx.subject) ||
      this.related.members.includes(ctx.subject),
  };
}

Permission Types

Tenant Permissions (Auto-Scoped)

Tenant permissions are the only permissions that support auto-scoping. The active tenant ID is automatically injected.

When you check a tenant# permission, OmniBase automatically uses the user's active tenant:

// In your roles.config.json
{
  "permissions": [
    "tenant#delete_tenant",   // Auto-scoped to active tenant
    "tenant#invite_user"      // Auto-scoped to active tenant
  ]
}

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 any custom entities:

// Creating a resource permission - UUID required
await permissionsApi.createRelationship({
  createRelationshipRequest: {
    namespace: 'Project',
    object: 'proj-123-uuid',  // Specific UUID required
    relation: 'can_write',
    subjectId: userId,
    subjectNamespace: 'User',
  },
});

// Checking a resource permission - UUID required
await permissionsApi.checkPermission({
  checkPermissionRequest: {
    namespace: 'Project',
    object: 'proj-123-uuid',  // Specific UUID required
    relation: 'can_write',
    subjectId: userId,
    subjectNamespace: 'User',
  },
});

Subject Sets

Subject sets allow you to grant permissions to groups of users based on their relationships:

// Instead of granting to each user individually:
(Project, proj-123, viewer, user:alice)
(Project, proj-123, viewer, user:bob)
(Project, proj-123, viewer, user:carol)

// Grant to all members of a tenant:
(Project, proj-123, viewers, Tenant:acme-corp#members)

Creating Subject Set Relationships

await permissionsApi.createRelationship({
  createRelationshipRequest: {
    namespace: 'Project',
    object: projectId,
    relation: 'viewers',
    // Subject set: all members of the tenant
    subjectId: tenantId,
    subjectNamespace: 'Tenant',
    subjectRelation: 'members',
  },
});

This means "all members of the tenant can view this project".


Hierarchical Permissions

Define permission inheritance across namespaces:

omnibase/permissions/organizations.ts
import type { Namespace, Context } from "@ory/keto-namespace-types";

class User implements Namespace {}
class Tenant implements Namespace {
  related: {
    parent: Organization;
    owners: User[];
  };
}

class Organization implements Namespace {
  related: {
    owners: User[];
    tenants: Tenant[];
  };

  permits = {
    // Org owners can manage all child tenants
    manage_tenants: (ctx: Context) =>
      this.related.owners.includes(ctx.subject),
  };
}

// In Tenant namespace, inherit from Organization
class TenantWithInheritance implements Namespace {
  related: {
    parent: Organization;
    owners: User[];
  };

  permits = {
    // Tenant owners OR org owners can delete
    delete: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.parent.permits.manage_tenants(ctx),
  };
}

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 and role definitions.

Verify

omnibase permissions list

Lists all registered namespaces and their relations.


Checking Permissions in Code

Direct Permission Check

import { V1PermissionsApi } from '@omnibase/core-js';

const permissionsApi = new V1PermissionsApi(config);

const { data } = await permissionsApi.checkPermission({
  checkPermissionRequest: {
    namespace: 'Project',
    object: projectId,
    relation: 'write',
    subjectId: userId,
    subjectNamespace: 'User',
  },
});

if (data.data.allowed) {
  // User can write to this project
}

Middleware Pattern

async function requirePermission(
  namespace: string,
  relation: string,
  getObject: (req: Request) => string
) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const { data } = await permissionsApi.checkPermission({
      checkPermissionRequest: {
        namespace,
        object: getObject(req),
        relation,
        subjectId: req.user.id,
        subjectNamespace: 'User',
      },
    });

    if (!data.data.allowed) {
      return res.status(403).json({ error: 'Forbidden' });
    }

    next();
  };
}

// Usage
app.delete('/projects/:id',
  requirePermission('Project', 'delete', (req) => req.params.id),
  deleteProjectHandler
);

Listing Available Permissions

Get all available permission definitions:

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(`  - ${namespace.name}#${relation.name}`);
    console.log(`    ${relation.description}`);
  }
}

Subject Types

SubjectDescription
UserHuman users with sessions
ApiKeyProgrammatic API keys

Some permissions may be restricted to specific subject types.


Example: Document Management System

A complete example with multiple namespaces:

omnibase/permissions/tenants.ts
class Tenant implements Namespace {
  related: {
    owners: User[];
    admins: User[];
    members: User[];
  };

  permits = {
    delete_tenant: (ctx: Context) =>
      this.related.owners.includes(ctx.subject),
    manage_members: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.admins.includes(ctx.subject),
    view: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.admins.includes(ctx.subject) ||
      this.related.members.includes(ctx.subject),
  };
}
omnibase/permissions/folders.ts
class Folder implements Namespace {
  related: {
    parent: Folder;
    owners: User[];
    editors: User[];
    viewers: User[];
  };

  permits = {
    delete: (ctx: Context) =>
      this.related.owners.includes(ctx.subject),
    create_document: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.editors.includes(ctx.subject),
    view: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.editors.includes(ctx.subject) ||
      this.related.viewers.includes(ctx.subject) ||
      this.related.parent?.permits.view(ctx),
  };
}
omnibase/permissions/documents.ts
class Document implements Namespace {
  related: {
    parent: Folder;
    owners: User[];
    editors: User[];
    viewers: User[];
  };

  permits = {
    delete: (ctx: Context) =>
      this.related.owners.includes(ctx.subject),
    edit: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.editors.includes(ctx.subject),
    view: (ctx: Context) =>
      this.related.owners.includes(ctx.subject) ||
      this.related.editors.includes(ctx.subject) ||
      this.related.viewers.includes(ctx.subject) ||
      this.related.parent?.permits.view(ctx),
  };
}

Next Steps

On this page