Access Control Provider
The Access Control Provider defines acan function that determines whether a user has permission to perform an action.
Interface Definition
Frompackages/core/src/contexts/accessControl/types.ts:82-85:
export type AccessControlProvider = {
can: (params: CanParams) => Promise<CanReturnType>;
options?: AccessControlOptions;
};
export type CanParams = {
resource?: string;
action: string;
params?: {
resource?: IResourceItem;
id?: BaseKey;
[key: string]: any;
};
};
export type CanReturnType = {
can: boolean;
reason?: string;
[key: string]: unknown;
};
Basic Implementation
import { AccessControlProvider } from "@refinedev/core";
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action, params }) => {
// Get current user role (from auth context, JWT, etc.)
const user = getCurrentUser();
// Admin has full access
if (user.role === "admin") {
return { can: true };
}
// Editor can read and write
if (user.role === "editor") {
if (action === "list" || action === "show") {
return { can: true };
}
if (action === "create" || action === "edit") {
return { can: true };
}
if (action === "delete") {
return {
can: false,
reason: "Editors cannot delete records",
};
}
}
// Viewer can only read
if (user.role === "viewer") {
if (action === "list" || action === "show") {
return { can: true };
}
return {
can: false,
reason: "Viewers have read-only access",
};
}
return { can: false };
},
};
Integration with Refine
import { Refine } from "@refinedev/core";
<Refine
dataProvider={dataProvider}
authProvider={authProvider}
accessControlProvider={accessControlProvider}
resources={[
{
name: "posts",
list: "/posts",
create: "/posts/create",
edit: "/posts/edit/:id",
show: "/posts/show/:id",
},
]}
/>
Using Access Control
useCan Hook
Check permissions programmatically:import { useCan } from "@refinedev/core";
function PostList() {
const { data: canCreate } = useCan({
resource: "posts",
action: "create",
});
const { data: canDelete } = useCan({
resource: "posts",
action: "delete",
});
return (
<div>
{canCreate?.can && (
<button onClick={handleCreate}>Create Post</button>
)}
<Table
dataSource={posts}
actions={(record) => (
<>
<EditButton />
{canDelete?.can && <DeleteButton />}
</>
)}
/>
</div>
);
}
CanAccess Component
Conditionally render components based on permissions:import { CanAccess } from "@refinedev/core";
function PostList() {
return (
<div>
<h1>Posts</h1>
<CanAccess resource="posts" action="create">
<CreateButton />
</CanAccess>
<Table
dataSource={posts}
rowActions={(record) => (
<>
<CanAccess resource="posts" action="edit" params={{ id: record.id }}>
<EditButton recordItemId={record.id} />
</CanAccess>
<CanAccess resource="posts" action="delete" params={{ id: record.id }}>
<DeleteButton recordItemId={record.id} />
</CanAccess>
</>
)}
/>
</div>
);
}
Fallback Content
Show alternative content when access is denied:<CanAccess
resource="posts"
action="create"
fallback={<div>You don't have permission to create posts</div>}
>
<CreateButton />
</CanAccess>
Resource-Based Permissions
Simple Role-Based Control
const permissions = {
admin: {
posts: ["list", "show", "create", "edit", "delete"],
users: ["list", "show", "create", "edit", "delete"],
settings: ["list", "show", "edit"],
},
editor: {
posts: ["list", "show", "create", "edit"],
users: ["list", "show"],
},
viewer: {
posts: ["list", "show"],
users: ["list"],
},
};
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action }) => {
const user = getCurrentUser();
const allowed = permissions[user.role]?.[resource]?.includes(action);
return {
can: !!allowed,
reason: allowed ? undefined : `${user.role}s cannot ${action} ${resource}`,
};
},
};
Attribute-Based Access Control (ABAC)
Check permissions based on record attributes:const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action, params }) => {
const user = getCurrentUser();
if (resource === "posts") {
// Anyone can list and view
if (action === "list" || action === "show") {
return { can: true };
}
// Check ownership for edit/delete
if (action === "edit" || action === "delete") {
const post = params?.resource?.meta?.post;
// Admin can edit/delete any post
if (user.role === "admin") {
return { can: true };
}
// Users can only edit/delete their own posts
if (post?.authorId === user.id) {
return { can: true };
}
return {
can: false,
reason: "You can only edit your own posts",
};
}
}
return { can: false };
},
};
function PostEdit({ id }) {
const { data: post } = useOne({ resource: "posts", id });
return (
<CanAccess
resource="posts"
action="edit"
params={{ resource: { meta: { post } }, id }}
>
<EditForm post={post} />
</CanAccess>
);
}
Integration with Popular RBAC Systems
Casbin
Fromexamples/access-control-casbin/src/App.tsx:44-72:
import { newEnforcer } from "casbin";
import { model, adapter } from "./casbin-config";
const role = localStorage.getItem("role") ?? "editor";
const accessControlProvider: AccessControlProvider = {
can: async ({ action, params, resource }) => {
const enforcer = await newEnforcer(model, adapter);
// Check resource-level permissions
if (action === "delete" || action === "edit" || action === "show") {
const can = await enforcer.enforce(
role,
`${resource}/${params?.id}`,
action,
);
return { can };
}
// Check field-level permissions
if (action === "field") {
const can = await enforcer.enforce(
role,
`${resource}/${params?.field}`,
action,
);
return { can };
}
// Check general resource permissions
const can = await enforcer.enforce(role, resource, action);
return { can };
},
};
# policy.csv
p, admin, posts, create
p, admin, posts, edit
p, admin, posts, delete
p, admin, posts, list
p, editor, posts, create
p, editor, posts, edit
p, editor, posts, list
p, viewer, posts, list
p, viewer, posts, show
Cerbos
import { GRPC as Cerbos } from "@cerbos/grpc";
const cerbos = new Cerbos("localhost:3593", { tls: false });
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action, params }) => {
const user = getCurrentUser();
const decision = await cerbos.check({
principal: {
id: user.id,
roles: [user.role],
attr: { department: user.department },
},
resource: {
kind: resource,
id: params?.id?.toString(),
attr: params?.resource?.meta || {},
},
actions: [action],
});
return {
can: decision.isAllowed(action),
reason: decision.isAllowed(action) ? undefined : "Access denied by policy",
};
},
};
Custom Permission Service
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action, params }) => {
const user = getCurrentUser();
// Fetch permissions from backend
const response = await fetch(
`${API_URL}/permissions/check`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${user.token}`,
},
body: JSON.stringify({
userId: user.id,
resource,
action,
context: params,
}),
},
);
const { allowed, reason } = await response.json();
return {
can: allowed,
reason,
};
},
};
Field-Level Permissions
Hide or disable specific fields based on permissions:function PostForm() {
const { data: canEditStatus } = useCan({
resource: "posts",
action: "field",
params: { field: "status" },
});
const { data: canEditFeatured } = useCan({
resource: "posts",
action: "field",
params: { field: "featured" },
});
return (
<Form>
<Input name="title" />
<Textarea name="content" />
{canEditStatus?.can && (
<Select name="status" options={statusOptions} />
)}
{canEditFeatured?.can && (
<Checkbox name="featured" />
)}
</Form>
);
}
Button-Level Access Control
Refine automatically handles button permissions whenaccessControlProvider is provided:
import { EditButton, DeleteButton, CreateButton } from "@refinedev/antd";
function PostList() {
return (
<div>
{/* Automatically checks "create" permission */}
<CreateButton resource="posts" />
<Table
dataSource={posts}
actions={(record) => (
<>
{/* Automatically checks "edit" permission */}
<EditButton resource="posts" recordItemId={record.id} />
{/* Automatically checks "delete" permission */}
<DeleteButton resource="posts" recordItemId={record.id} />
</>
)}
/>
</div>
);
}
Configure Button Behavior
<Refine
accessControlProvider={accessControlProvider}
options={{
accessControl: {
buttons: {
enableAccessControl: true, // Enable automatic checks
hideIfUnauthorized: true, // Hide buttons if unauthorized (default: false)
},
},
}}
/>
Permissions from Auth Provider
Combine withgetPermissions from auth provider:
// Auth Provider
const authProvider: AuthProvider = {
getPermissions: async () => {
const user = getCurrentUser();
return {
role: user.role,
permissions: user.permissions, // e.g., ["posts.create", "posts.edit"]
};
},
};
// Access Control Provider
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action }) => {
const permissions = await authProvider.getPermissions();
const requiredPermission = `${resource}.${action}`;
const hasPermission = permissions.permissions.includes(requiredPermission);
return {
can: hasPermission,
reason: hasPermission ? undefined : "Insufficient permissions",
};
},
};
import { usePermissions } from "@refinedev/core";
function MyComponent() {
const { data: permissions } = usePermissions();
return (
<div>
<p>Role: {permissions?.role}</p>
<p>Permissions: {permissions?.permissions.join(", ")}</p>
</div>
);
}
Conditional Resource Display
Hide resources from sidebar based on permissions:import { useMenu } from "@refinedev/core";
function Sidebar() {
const { menuItems } = useMenu();
// menuItems are automatically filtered based on permissions
// Resources are checked with "list" action by default
return (
<nav>
{menuItems.map((item) => (
<a key={item.key} href={item.route}>
{item.icon} {item.label}
</a>
))}
</nav>
);
}
Advanced Patterns
Dynamic Permissions
Fetch permissions dynamically:const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action, params }) => {
const user = getCurrentUser();
// Fetch real-time permissions from API
const response = await fetch(
`${API_URL}/users/${user.id}/can/${action}/${resource}`,
{
headers: { Authorization: `Bearer ${user.token}` },
},
);
const { can, reason } = await response.json();
return { can, reason };
},
};
Hierarchical Roles
const roleHierarchy = {
superadmin: ["admin", "editor", "viewer"],
admin: ["editor", "viewer"],
editor: ["viewer"],
viewer: [],
};
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action }) => {
const user = getCurrentUser();
const userRoles = [user.role, ...roleHierarchy[user.role]];
// Check if any of user's roles (including inherited) have permission
for (const role of userRoles) {
const allowed = permissions[role]?.[resource]?.includes(action);
if (allowed) {
return { can: true };
}
}
return { can: false, reason: "Insufficient permissions" };
},
};
Time-Based Access
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action, params }) => {
const user = getCurrentUser();
// Check time-based restrictions
const hour = new Date().getHours();
if (action === "delete" && (hour < 9 || hour > 17)) {
return {
can: false,
reason: "Delete operations are only allowed during business hours (9 AM - 5 PM)",
};
}
// Regular permission check
return checkPermissions(user, resource, action);
},
};
Context-Aware Permissions
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action, params }) => {
const user = getCurrentUser();
if (resource === "posts" && action === "edit") {
const post = params?.resource?.meta?.post;
// Check multiple conditions
const isOwner = post?.authorId === user.id;
const isAdmin = user.role === "admin";
const isPublished = post?.status === "published";
const withinEditWindow =
Date.now() - new Date(post?.createdAt).getTime() < 24 * 60 * 60 * 1000;
// Allow if admin, or owner within 24h of creation
if (isAdmin || (isOwner && withinEditWindow)) {
return { can: true };
}
if (isOwner && !withinEditWindow) {
return {
can: false,
reason: "You can only edit your posts within 24 hours of creation",
};
}
return {
can: false,
reason: "You don't have permission to edit this post",
};
}
return { can: false };
},
};
Best Practices
1. Fail Secure
Default to denying access:const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action }) => {
try {
// Permission check logic
return checkPermission(resource, action);
} catch (error) {
// On error, deny access
console.error("Permission check failed:", error);
return {
can: false,
reason: "Unable to verify permissions",
};
}
},
};
2. Cache Permissions
import { useQuery } from "@tanstack/react-query";
function useCachedCan(params: CanParams) {
return useQuery({
queryKey: ["permissions", params.resource, params.action, params.params],
queryFn: () => accessControlProvider.can(params),
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
});
}
3. Provide Clear Feedback
<CanAccess
resource="posts"
action="delete"
fallback={(data) => (
<Tooltip title={data?.reason || "Access denied"}>
<Button disabled>Delete</Button>
</Tooltip>
)}
>
<DeleteButton />
</CanAccess>
4. Server-Side Validation
Always validate permissions on the server:// Client-side (UI)
<CanAccess resource="posts" action="delete">
<DeleteButton />
</CanAccess>
// Server-side (API)
app.delete("/posts/:id", async (req, res) => {
// ALWAYS check permissions on server
const canDelete = await checkPermission(req.user, "posts", "delete");
if (!canDelete) {
return res.status(403).json({ error: "Forbidden" });
}
// Proceed with deletion
});
5. Type Safety
import { AccessControlProvider, CanParams } from "@refinedev/core";
type Action = "list" | "show" | "create" | "edit" | "delete";
type Resource = "posts" | "users" | "categories";
const accessControlProvider: AccessControlProvider = {
can: async ({ resource, action, params }: CanParams) => {
// Type-safe permission checks
const typedResource = resource as Resource;
const typedAction = action as Action;
return checkPermission(typedResource, typedAction);
},
};
Testing Access Control
import { renderHook } from "@testing-library/react";
import { useCan } from "@refinedev/core";
describe("Access Control", () => {
it("should allow admin to delete posts", async () => {
// Mock user as admin
mockCurrentUser({ role: "admin" });
const { result } = renderHook(() =>
useCan({
resource: "posts",
action: "delete",
}),
);
await waitFor(() => {
expect(result.current.data?.can).toBe(true);
});
});
it("should prevent viewer from creating posts", async () => {
// Mock user as viewer
mockCurrentUser({ role: "viewer" });
const { result } = renderHook(() =>
useCan({
resource: "posts",
action: "create",
}),
);
await waitFor(() => {
expect(result.current.data?.can).toBe(false);
expect(result.current.data?.reason).toBeDefined();
});
});
});
Next Steps
Authentication
Learn about authentication
Auth Provider API
Auth provider reference
Access Control Hooks
Access control hook reference
Casbin Tutorial
Implement Casbin RBAC