Role-Based Access Control
Implement RBAC with role hierarchies and multi-role support using hasRole() in @zap-studio/permit.
Role-Based Access Control (RBAC) is a common authorization pattern where permissions are assigned to roles, and users are assigned roles. @zap-studio/permit provides utilities for implementing RBAC with support for role hierarchies.
Understanding RBAC
In RBAC, instead of assigning permissions directly to users, you:
- Define roles (e.g., "guest", "user", "admin")
- Assign permissions to roles (e.g., admin can delete posts)
- Assign roles to users (e.g., Alice is an admin)
This simplifies permission management — when you need to change what admins can do, you update the role's permissions once instead of updating every admin user.
Simple Role Checks
The hasRole() function checks if the current user has a specific role:
import { hasRole } from "@zap-studio/permit";
hasRole(role)Context Requirements
Your context must include a role property that can be:
- A single role:
{ role: "admin" } - An array of roles:
{ role: ["user", "moderator"] }
Example: Basic RBAC
import { z } from "zod";
import { createPolicy, when, hasRole, or } from "@zap-studio/permit";
import type { Resources, Actions } from "@zap-studio/permit/types";
const resources = {
post: z.object({
id: z.string(),
authorId: z.string(),
status: z.enum(["draft", "published", "archived"]),
}),
user: z.object({
id: z.string(),
email: z.string(),
}),
} satisfies Resources;
const actions = {
post: ["read", "write", "delete", "publish"],
user: ["read", "update", "delete", "ban"],
} as const satisfies Actions<typeof resources>;
type Role = "guest" | "user" | "moderator" | "admin";
type AppContext = {
user: { id: string } | null;
role: Role;
};
const policy = createPolicy<AppContext>({
resources,
actions,
rules: {
post: {
// Guests can read published posts
read: when((ctx, _, post) =>
post.status === "published" || ctx.role === "user"
),
// Users can write their own posts
write: when((ctx, _, post) =>
ctx.role === "user" && ctx.user?.id === post.authorId
),
// Moderators and admins can delete any post
delete: when(
or(
hasRole("moderator"),
hasRole("admin")
)
),
// Only admins can publish
publish: when(hasRole("admin")),
},
user: {
// Users can read their own profile, admins can read any
read: when(
or(
(ctx, _, user) => ctx.user?.id === user.id,
hasRole("admin")
)
),
// Users can update their own profile
update: when((ctx, _, user) => ctx.user?.id === user.id),
// Only admins can delete users
delete: when(hasRole("admin")),
// Moderators and admins can ban users
ban: when(
or(
hasRole("moderator"),
hasRole("admin")
)
),
},
},
});For now, hasRole() returns a ConditionFn, which can be used in when() conditions. Thus, it doesn't return a boolean value directly. Prefer using role directly in conditions.
Role Hierarchies
Often, higher-level roles inherit permissions from lower-level roles. For example, an admin should have all permissions that a user has, plus admin-specific ones.
Defining a Role Hierarchy
A role hierarchy maps each role to an array of roles it inherits from:
import type { RoleHierarchy } from "@zap-studio/permit/types";
type Role = "guest" | "user" | "moderator" | "admin";
const roleHierarchy: RoleHierarchy<Role> = {
guest: [], // No inherited roles
user: ["guest"], // Users inherit guest permissions
moderator: ["user"], // Moderators inherit user permissions
admin: ["moderator"], // Admins inherit moderator permissions
};
// Permission inheritance chain:
// admin → moderator → user → guestUsing hasRole with Hierarchy
Pass the hierarchy as the second argument to hasRole():
import { hasRole } from "@zap-studio/permit";
// Without hierarchy: only matches exact role
hasRole("user") // Only true if role === "user"
// With hierarchy: matches role or any role that inherits from it
hasRole("user", roleHierarchy) // True for "user", "moderator", or "admin"Multiple Roles
Users can have multiple roles. The context can include an array of roles:
type AppContext = {
user: { id: string } | null;
role: Role[]; // Array of roles
};
const context: AppContext = {
user: { id: "user-1" },
role: ["user", "beta-tester"], // User has both roles
};hasRole() checks if any of the user's roles match (or inherit from) the required role.
Example: Multiple Roles
import { z } from "zod";
import { createPolicy, when, hasRole, or } from "@zap-studio/permit";
import type { Resources, Actions, RoleHierarchy } from "@zap-studio/permit/types";
const resources = {
betaFeature: z.object({
id: z.string(),
name: z.string(),
}),
supportTicket: z.object({
id: z.string(),
userId: z.string(),
priority: z.enum(["low", "normal", "high", "urgent"]),
}),
} satisfies Resources;
const actions = {
betaFeature: ["access"],
supportTicket: ["create", "escalate"],
} as const satisfies Actions<typeof resources>;
type Role = "user" | "beta-tester" | "premium" | "support-agent" | "admin";
const roleHierarchy: RoleHierarchy<Role> = {
user: [],
"beta-tester": ["user"],
premium: ["user"],
"support-agent": ["user"],
admin: ["premium", "support-agent"], // Admin inherits from multiple roles
};
type MultiRoleContext = {
user: { id: string } | null;
role: Role[];
};
const multiRolePolicy = createPolicy<MultiRoleContext>({
resources,
actions,
rules: {
betaFeature: {
// Only beta testers can access beta features
access: when(hasRole("beta-tester", roleHierarchy)),
},
supportTicket: {
// Any authenticated user can create tickets
create: when(hasRole("user", roleHierarchy)),
// Only premium users or support agents can escalate
escalate: when(
or(
hasRole("premium", roleHierarchy),
hasRole("support-agent", roleHierarchy)
)
),
},
},
});
// User with multiple roles
const betaPremiumUser: MultiRoleContext = {
user: { id: "u1" },
role: ["user", "beta-tester", "premium"],
};
const ticket = { id: "t1", userId: "u1", priority: "normal" as const };
const feature = { id: "f1", name: "New Dashboard" };
console.log(multiRolePolicy.can(betaPremiumUser, "access", "betaFeature", feature)); // true (has beta-tester)
console.log(multiRolePolicy.can(betaPremiumUser, "escalate", "supportTicket", ticket)); // true (has premium)Collecting Inherited Roles
The collectInheritedRoles() function returns all roles a user has, including inherited ones:
import { collectInheritedRoles } from "@zap-studio/permit";
collectInheritedRoles(roles, hierarchy)Example: Role Inspection
import { collectInheritedRoles } from "@zap-studio/permit";
import type { RoleHierarchy } from "@zap-studio/permit/types";
type Role = "guest" | "user" | "moderator" | "admin";
const hierarchy: RoleHierarchy<Role> = {
guest: [],
user: ["guest"],
moderator: ["user"],
admin: ["moderator"],
};
// Get all roles for an admin
const adminRoles = collectInheritedRoles(["admin"], hierarchy);
console.log(adminRoles); // Set { "admin", "moderator", "user", "guest" }
// Get all roles for a moderator
const modRoles = collectInheritedRoles(["moderator"], hierarchy);
console.log(modRoles); // Set { "moderator", "user", "guest" }
// Multiple input roles
const mixedRoles = collectInheritedRoles(["user", "beta-tester"], {
...hierarchy,
"beta-tester": [],
});
console.log(mixedRoles); // Set { "user", "guest", "beta-tester" }Best Practices
- Keep hierarchies simple — Deep hierarchies are hard to reason about
- Document role permissions — Maintain a clear mapping of what each role can do
- Use meaningful role names — "editor" is clearer than "level-3"
- Avoid circular dependencies — Role A should not inherit from role B if B inherits from A
- Test edge cases — Especially when users have multiple roles
Last updated on