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
| Role | Description |
|---|---|
| superadmin | Full system access, can manage other admins |
| admin | Admin dashboard access, user management |
| user | Basic 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
- Use role hierarchy - Assign users to specific roles, not individual permissions
- Check permissions server-side - Never trust client-side permission checks alone
- Audit permission changes - Log all policy modifications
- Least privilege - Start with minimal permissions, add as needed