Skip to main content
Refine provides a flexible authorization system through the Access Control Provider. It enables fine-grained, role-based access control (RBAC) for resources, actions, and even individual UI elements.

Access Control Provider

The Access Control Provider defines a can function that determines whether a user has permission to perform an action.

Interface Definition

From packages/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 };
  },
};
Usage in component:
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>
  );
}

Casbin

From examples/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 };
  },
};
Casbin policy file:
# 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 when accessControlProvider 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 with getPermissions 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",
    };
  },
};
Usage:
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

Build docs developers (and LLMs) love