Skip to content

Role-Based Access Control (RBAC)

Kyrin's RBAC system lets you define roles, assign permissions, and check authorization with support for role inheritance.

Overview

RBAC in Kyrin is built around three core concepts:

  • Roles — Named sets of permissions (e.g., "admin", "moderator", "user")
  • Permissions — String identifiers that represent actions (e.g., "read:all", "write:posts")
  • Users — Identities that get assigned one or more roles

Defining Roles

Roles are defined with a name, a list of permissions, and optional parent roles to inherit from.

typescript
import { createAuth } from "kyrin";

const auth = createAuth({ jwtSecret: "my-secret" });

// Basic roles
auth.roles.define("admin", ["*"]);                    // Wildcard = all permissions
auth.roles.define("moderator", ["read:all", "write:posts", "delete:posts"]);
auth.roles.define("user", ["read:own", "update:own"]);
auth.roles.define("guest", ["read:public"]);

// Role inheritance (superadmin inherits all admin permissions)
auth.roles.define("superadmin", ["manage:users"], ["admin"]);

define(name, permissions, inherits?)

ParameterTypeDescription
namestringRole name (must be unique)
permissionsstring[]List of permission strings. Use "*" for all permissions
inheritsstring[]Optional. Parent role names to inherit permissions from

The "*" wildcard permission grants access to every permission check. This is typically reserved for admin roles.

Assigning Roles to Users

typescript
// Assign roles to a user
auth.roles.assign("user-42", ["user"]);
auth.roles.assign("admin-1", ["admin"]);
auth.roles.assign("super-1", ["superadmin"]);

// A user can have multiple roles
auth.roles.assign("mod-1", ["moderator", "user"]);

Checking Permissions

hasPermission(userId, permission)

Check if a user has a specific permission.

typescript
// Define roles
auth.roles.define("user", ["read:own", "update:own"]);
auth.roles.assign("user-42", ["user"]);

// Check
const canRead = auth.roles.hasPermission("user-42", "read:own");
console.log(canRead); // true

const canDelete = auth.roles.hasPermission("user-42", "delete:posts");
console.log(canDelete); // false

hasAnyPermission(userId, permissions)

Check if a user has at least one of the given permissions.

typescript
const canWrite = auth.roles.hasAnyPermission("user-42", [
  "write:posts",
  "write:all",
  "*"
]);

hasAllPermissions(userId, permissions)

Check if a user has all of the given permissions.

typescript
const canManage = auth.roles.hasAllPermissions("admin-1", [
  "read:all",
  "write:posts",
  "delete:posts",
]);

Checking Roles

hasRole(userId, roleName)

Check if a user has a specific role.

typescript
const isAdmin = auth.roles.hasRole("admin-1", "admin");
console.log(isAdmin); // true

hasAnyRole(userId, roleNames)

Check if a user has at least one of the given roles.

typescript
const isPrivileged = auth.roles.hasAnyRole("user-42", [
  "admin",
  "moderator",
]);

Listing and Inspecting

typescript
// Get all defined roles
const allRoles = auth.roles.list();
// Returns: [{ name: "admin", permissions: ["*"] }, ...]

// Get a specific role definition
const adminRole = auth.roles.get("admin");
// Returns: { name: "admin", permissions: ["*"], inherits?: string[] }

// Get all roles assigned to a user
const userRoles = auth.roles.getNames("user-42");
// Returns: ["user"]

// Get all permissions a user has (including inherited)
const permissions = auth.roles.getPermissions("super-1");
// Returns: ["manage:users", "*"] (own + inherited from admin)

Revoking and Clearing

typescript
// Remove all roles from a user
auth.roles.revoke("user-42");
console.log(auth.roles.getNames("user-42")); // []

// Clear all role definitions and assignments
auth.roles.clear();
// After clear(), no roles exist — you must redefine them

Standalone RBAC Functions

You can also use the RBAC functions directly without the Auth class.

typescript
import {
  defineRole,
  getRole,
  getAllRoles,
  setUserRoles,
  getUserRoleNames,
  getUserPermissions,
  hasPermission,
  hasAnyPermission,
  hasAllPermissions,
  hasRole,
  hasAnyRole,
  removeUserRoles,
} from "kyrin";

defineRole("editor", ["read:all", "write:posts", "write:pages"]);
setUserRoles("user-42", ["editor"]);

if (hasPermission("user-42", "write:pages")) {
  console.log("User can edit pages");
}

Permission Naming Convention

While you can use any string for permissions, we recommend a action:resource convention:

ConventionExampleMeaning
action:resourceread:postsRead posts
action:resourcewrite:postsCreate/update posts
action:resourcedelete:postsDelete posts
action:allread:allRead everything
**All permissions (admin only)

Complete Example

typescript
import { createAuth } from "kyrin";
import { createApp } from "kyrin";

const auth = createAuth({ jwtSecret: process.env.JWT_SECRET! });
const app = createApp();

// --- Define roles ---
auth.roles.define("admin", ["*"]);
auth.roles.define("moderator", ["read:all", "write:posts", "delete:posts"]);
auth.roles.define("user", ["read:own", "update:own"]);

// --- Auth middleware ---
app.use(async (c, next) => {
  const token = c.req.headers.get("Authorization")?.slice(7);
  if (!token) return c.json({ error: "Unauthorized" }, 401);

  const payload = await auth.verifyAccessToken(token);
  if (!payload) return c.json({ error: "Invalid token" }, 401);

  c.store.userId = payload.sub;
  c.store.role = payload.role;
  await next();
});

// --- Authorization middleware factory ---
function requirePermission(permission: string) {
  return async (c: any, next: () => Promise<void>) => {
    if (!auth.roles.hasPermission(c.store.userId, permission)) {
      return c.json({ error: "Forbidden" }, 403);
    }
    await next();
  };
}

// --- Routes ---
app.get("/posts", async (c) => {
  // Any authenticated user can read their own posts
  if (!auth.roles.hasPermission(c.store.userId, "read:own")) {
    return c.json({ error: "Forbidden" }, 403);
  }
  return c.json(await getPosts(c.store.userId));
});

app.post("/posts", requirePermission("write:posts"), async (c) => {
  const data = await c.body();
  return c.json(await createPost(data));
});

app.delete("/posts/:id", requirePermission("delete:posts"), async (c) => {
  await deletePost(c.param("id"));
  return c.json({ success: true });
});

// Admin-only route (wildcard permission)
app.get("/admin/logs", requirePermission("*"), async (c) => {
  return c.json(await getLogs());
});

Role Inheritance

Roles can inherit permissions from other roles. This creates a hierarchy:

typescript
auth.roles.define("user", ["read:own", "update:own"]);
auth.roles.define("moderator", ["read:all", "write:posts", "delete:posts"], ["user"]);
auth.roles.define("admin", ["*"], ["moderator"]);

// admin inherits all moderator and user permissions
auth.roles.assign("admin-1", ["admin"]);

// This is true because admin inherits from moderator which inherits from user
auth.roles.hasPermission("admin-1", "read:own");  // true
auth.roles.hasPermission("admin-1", "delete:posts"); // true
auth.roles.hasPermission("admin-1", "*"); // true

Default Roles

Kyrin comes with three built-in roles:

RolePermissionsDescription
admin["*"]Full access to everything
user["read:own", "update:own"]Standard user access
guest["read:public"]Read-only public access

You can override or remove these by calling clear() and redefining them.

Next Steps

Released under the MIT License.