Docs/modules/RBAC (Role-Based Access Control)

RBAC (Role-Based Access Control)

NextShip uses Casbin for flexible, policy-based access control.

Features

  • Role hierarchy (superadmin > admin > user)
  • Dynamic policy management
  • Admin UI for permission configuration
  • Database-backed policy storage

Role Hierarchy

superadmin
    └── admin
          └── user
RoleDescription
superadminFull system access, can manage other admins
adminAdmin dashboard access, user management
userBasic access to own resources

Roles inherit permissions from lower roles. An admin has all user permissions plus admin-specific ones.

How It Works

Policy Model

Casbin uses a PERM model (Policy, Effect, Request, Matchers):

# config/rbac_model.conf
[request_definition]
r = sub, obj, act
 
[policy_definition]
p = sub, obj, act
 
[role_definition]
g = _, _
 
[policy_effect]
e = some(where (p.eft == allow))
 
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
  • sub: Subject (user ID or role)
  • obj: Object (resource like "users", "payments")
  • act: Action ("read", "write", "delete")

Default Policies

Default policies are defined in config/rbac-defaults.json:

{
  "policies": [
    ["admin", "admin", "access"],
    ["admin", "users", "read"],
    ["admin", "users", "write"],
    ["admin", "audit-logs", "read"],
    ["admin", "payments", "read"],
    ["admin", "emails", "read"],
    ["user", "dashboard", "access"],
    ["user", "settings", "read"],
    ["user", "settings", "write"]
  ],
  "roleHierarchy": [
    ["superadmin", "admin"],
    ["admin", "user"]
  ]
}

Server Actions

Permission Checks

// src/server/actions/permission.ts
 
// Check if current user has permission
export async function checkUserPermission(object: string, action: string) {
  const session = await requireAuth();
  const enforcer = await getEnforcer();
  return enforcer.enforce(session.user.id, object, action);
}
 
// Check if current user has role
export async function checkUserRole(role: string) {
  const session = await requireAuth();
  const enforcer = await getEnforcer();
  return enforcer.hasRoleForUser(session.user.id, role);
}
 
// Get current user's roles
export async function getMyRoles() {
  const session = await requireAuth();
  const enforcer = await getEnforcer();
  return enforcer.getRolesForUser(session.user.id);
}
 
// Get current user's permissions
export async function getMyPermissions() {
  const session = await requireAuth();
  const enforcer = await getEnforcer();
  return enforcer.getPermissionsForUser(session.user.id);
}

Admin Operations

// Assign role to user
export async function assignUserRole(userId: string, role: string) {
  await requireAdmin();
  const enforcer = await getEnforcer();
  await enforcer.addRoleForUser(userId, role);
  await enforcer.savePolicy();
}
 
// Remove role from user
export async function removeUserRole(userId: string, role: string) {
  await requireAdmin();
  const enforcer = await getEnforcer();
  await enforcer.deleteRoleForUser(userId, role);
  await enforcer.savePolicy();
}
 
// Add permission policy
export async function addPermissionPolicy(subject: string, object: string, action: string) {
  await requireAdmin();
  const enforcer = await getEnforcer();
  await enforcer.addPolicy(subject, object, action);
  await enforcer.savePolicy();
}
 
// Remove permission policy
export async function removePermissionPolicy(subject: string, object: string, action: string) {
  await requireAdmin();
  const enforcer = await getEnforcer();
  await enforcer.removePolicy(subject, object, action);
  await enforcer.savePolicy();
}

Usage Examples

Protecting Pages

// src/app/[locale]/(dashboard)/users/page.tsx
import { requireAdmin } from "@/lib/permissions";
 
export default async function UsersPage() {
  await requireAdmin();
  // Only admins can reach here
}

Protecting Server Actions

// src/server/actions/admin.ts
import { requirePermission } from "@/lib/permissions";
 
export async function deleteUser(userId: string) {
  await requirePermission("users", "delete");
  // Only users with "users:delete" permission can execute
}

Client-Side Role Checks

"use client";
 
import { usePermissions } from "@/hooks/use-permissions";
 
export function AdminButton() {
  const { hasRole, isLoading } = usePermissions();
 
  if (isLoading) return null;
  if (!hasRole("admin")) return null;
 
  return <Button>Admin Action</Button>;
}

Admin Dashboard

The permissions page (/permissions) allows admins to:

  • View all policies
  • Add/remove policies
  • Assign/remove roles from users
  • View user permissions

Database Storage

Policies are stored in the casbin_rule table:

// src/lib/db/schema.ts
export const casbinRule = pgTable("casbin_rule", {
  id: serial("id").primaryKey(),
  ptype: text("ptype").notNull(),
  v0: text("v0"),
  v1: text("v1"),
  v2: text("v2"),
  v3: text("v3"),
  v4: text("v4"),
  v5: text("v5"),
});

Initializing Default Policies

On first run or reset, initialize default policies:

import { initializeDefaultPolicies } from "@/server/actions/permission";
 
// In admin setup or seed script
await initializeDefaultPolicies();

Best Practices

  1. Use role hierarchy - Assign users to specific roles, not individual permissions
  2. Check permissions server-side - Never trust client-side permission checks alone
  3. Audit permission changes - Log all policy modifications
  4. Least privilege - Start with minimal permissions, add as needed