Skip to main content
Access control is critical for securing enterprise applications. Refine provides a flexible and agnostic API through the accessControlProvider that allows you to integrate different access control methods like RBAC (Role-Based Access Control), ABAC (Attribute-Based Access Control), ACL (Access Control Lists), and more.

Access Control Provider

The accessControlProvider is the core interface for managing access control in Refine. At minimum, it must implement a can method:
import { AccessControlProvider } from "@refinedev/core";

export const accessControlProvider: AccessControlProvider = {
  can: async ({ resource, action, params }) => {
    // Your access control logic
    return { can: true };
  },
  options: {
    buttons: {
      enableAccessControl: true,
      hideIfUnauthorized: false,
    },
  },
};

Parameters

  • resource: The resource being accessed (e.g., “posts”, “users”)
  • action: The action being performed (e.g., “list”, “create”, “edit”, “delete”, “show”)
  • params: Additional parameters including:
    • id: The record ID (for edit, show, delete actions)
    • resource: The full resource object with metadata
    • Custom parameters passed via meta

Return Value

interface CanReturnType {
  can: boolean;
  reason?: string; // Optional reason for denial (shown in tooltips)
}

Integrating with Casbin

Casbin is a powerful authorization library that supports various access control models. Here’s how to integrate it with Refine:

Installation

npm install casbin

Setting up Models and Policies

import { newModel, StringAdapter, newEnforcer } from "casbin";

// Define the access control model
export const model = newModel(`
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act, eft

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))

[matchers]
m = g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)
`);

// Define policies
export const adapter = new StringAdapter(`
p, admin, posts, (list)|(create)
p, admin, posts/*, (edit)|(show)|(delete)
p, admin, users, (list)|(create)
p, admin, users/*, (edit)|(show)|(delete)

p, editor, posts, (list)|(create)
p, editor, posts/*, (edit)|(show)
p, editor, posts/hit, field, deny

p, viewer, posts, list
p, viewer, posts/*, show
`);

Implementing the Access Control Provider

import { AccessControlProvider } from "@refinedev/core";
import { newEnforcer } from "casbin";
import { model, adapter } from "./accessControl";

export const accessControlProvider: AccessControlProvider = {
  can: async ({ resource, action, params }) => {
    const enforcer = await newEnforcer(model, adapter);
    const role = localStorage.getItem("role") ?? "viewer";

    // Handle ID-based access (edit, show, delete)
    if (action === "delete" || action === "edit" || action === "show") {
      const can = await enforcer.enforce(
        role,
        `${resource}/${params?.id}`,
        action
      );
      return { can };
    }

    // Handle field-level access
    if (action === "field") {
      const can = await enforcer.enforce(
        role,
        `${resource}/${params?.field}`,
        action
      );
      return { can };
    }

    // Handle standard resource actions
    const can = await enforcer.enforce(role, resource, action);
    return { can };
  },
};

Role-Based Access Control (RBAC)

RBAC assigns permissions based on user roles:
type Role = "admin" | "editor" | "viewer";

const rolePermissions: Record<Role, string[]> = {
  admin: ["posts:*", "users:*", "categories:*"],
  editor: ["posts:list", "posts:create", "posts:edit", "posts:show"],
  viewer: ["posts:list", "posts:show"],
};

export const accessControlProvider: AccessControlProvider = {
  can: async ({ resource, action, params }) => {
    const role = getUserRole(); // Get from auth context
    const permissions = rolePermissions[role] || [];
    
    const permission = `${resource}:${action}`;
    const wildcardPermission = `${resource}:*`;
    
    const can = permissions.includes(permission) || 
                permissions.includes(wildcardPermission);
    
    return {
      can,
      reason: can ? undefined : `Role '${role}' cannot ${action} ${resource}`,
    };
  },
};

Attribute-Based Access Control (ABAC)

ABAC grants permissions based on attributes of the user, resource, and environment:
export const accessControlProvider: AccessControlProvider = {
  can: async ({ resource, action, params }) => {
    const user = await getUser();
    
    // Example: Users can only edit their own posts
    if (resource === "posts" && action === "edit") {
      const post = await dataProvider.getOne({
        resource: "posts",
        id: params?.id,
      });
      
      return {
        can: post.data.authorId === user.id,
        reason: "You can only edit your own posts",
      };
    }
    
    // Example: Users in the same department can view each other's data
    if (resource === "users" && action === "show") {
      const targetUser = await dataProvider.getOne({
        resource: "users",
        id: params?.id,
      });
      
      return {
        can: targetUser.data.department === user.department,
        reason: "You can only view users in your department",
      };
    }
    
    return { can: true };
  },
};

Field-Level Access Control

Control access to specific fields within a resource:
// In your access control provider
if (action === "field") {
  const enforcer = await newEnforcer(model, adapter);
  const role = getUserRole();
  
  const can = await enforcer.enforce(
    role,
    `${resource}/${params?.field}`,
    action
  );
  
  return { can };
}

// In your component
import { useCan } from "@refinedev/core";

const PostList = () => {
  const { tableProps } = useTable();
  const { data: canAccessHit } = useCan({
    resource: "posts",
    action: "field",
    params: { field: "hit" },
  });
  
  return (
    <Table {...tableProps}>
      <Table.Column dataIndex="id" title="ID" />
      <Table.Column dataIndex="title" title="Title" />
      
      {canAccessHit?.can && (
        <Table.Column
          dataIndex="hit"
          title="Views"
          render={(value) => <NumberField value={value} />}
        />
      )}
    </Table>
  );
};

Using useCan Hook

The useCan hook allows you to check permissions anywhere in your components:
import { useCan } from "@refinedev/core";

const MyComponent = () => {
  const { data } = useCan({
    resource: "posts",
    action: "delete",
    params: { id: 123 },
  });
  
  if (!data?.can) {
    return <div>Access Denied: {data?.reason}</div>;
  }
  
  return <DeleteButton recordItemId={123} />;
};

Caching Permissions

Use React Query options to cache permission checks:
const { data } = useCan({
  resource: "posts",
  action: "edit",
  params: { id: 123 },
  queryOptions: {
    staleTime: 5 * 60 * 1000, // 5 minutes
    cacheTime: 10 * 60 * 1000, // 10 minutes
  },
});

Using <CanAccess /> Component

Wrap components that require access control:
import { CanAccess } from "@refinedev/core";

const PostEdit = () => {
  return (
    <CanAccess
      resource="posts"
      action="edit"
      params={{ id: 123 }}
      fallback={<div>You don't have permission to edit this post</div>}
    >
      <EditForm />
    </CanAccess>
  );
};

Button Access Control

Refine automatically controls button visibility and state based on permissions:
import { EditButton, DeleteButton, ShowButton } from "@refinedev/antd";

const PostList = () => {
  return (
    <List>
      <Table>
        <Table.Column
          title="Actions"
          render={(_, record) => (
            <Space>
              {/* These buttons will be disabled/hidden based on permissions */}
              <EditButton recordItemId={record.id} />
              <ShowButton recordItemId={record.id} />
              <DeleteButton recordItemId={record.id} />
            </Space>
          )}
        />
      </Table>
    </List>
  );
};

Configuring Button Behavior

export const accessControlProvider: AccessControlProvider = {
  can: async ({ resource, action, params }) => {
    // Your logic
  },
  options: {
    buttons: {
      enableAccessControl: true,
      hideIfUnauthorized: true, // Hide instead of disable
    },
  },
};
Button-level access control is a UI convenience. Always enforce access control on the server side to ensure security.
Resources without list permission won’t appear in the sidebar:
// This resource won't show in the menu if user lacks 'list' permission
const resources = [
  {
    name: "posts",
    list: "/posts",
    create: "/posts/create",
  },
];

Integrating with Cerbos

Cerbos is a powerful authorization service:
import { GRPC as Cerbos } from "@cerbos/grpc";

const cerbos = new Cerbos("localhost:3593", { tls: false });

export const accessControlProvider: AccessControlProvider = {
  can: async ({ resource, action, params }) => {
    const user = await getUser();
    
    const decision = await cerbos.check({
      principal: {
        id: user.id,
        roles: user.roles,
        attr: user.attributes,
      },
      resource: {
        kind: resource,
        id: params?.id || "new",
      },
      actions: [action],
    });
    
    return {
      can: decision.isAllowed(action),
    };
  },
};

Resource-Level Metadata for Access Control

Use resource metadata for fine-grained control:
const resources = [
  {
    name: "posts",
    list: "/posts",
    meta: {
      requiredRole: "editor",
      department: "content",
    },
  },
];

export const accessControlProvider: AccessControlProvider = {
  can: async ({ resource, action, params }) => {
    const resourceMeta = params?.resource?.meta;
    const user = await getUser();
    
    if (resourceMeta?.requiredRole) {
      return {
        can: user.roles.includes(resourceMeta.requiredRole),
        reason: `Requires ${resourceMeta.requiredRole} role`,
      };
    }
    
    return { can: true };
  },
};

Performance Optimization

  1. Cache permission checks: Use React Query’s caching
  2. Batch permission checks: Check multiple permissions at once
  3. Lazy load permissions: Load permissions only when needed
  4. Server-side caching: Cache permission decisions on the server
// Example: Batch permission checks
const { data: permissions } = useQuery({
  queryKey: ["permissions", resource],
  queryFn: async () => {
    const actions = ["list", "create", "edit", "delete", "show"];
    const results = await Promise.all(
      actions.map(action =>
        accessControlProvider.can({ resource, action, params: {} })
      )
    );
    
    return actions.reduce((acc, action, index) => {
      acc[action] = results[index].can;
      return acc;
    }, {} as Record<string, boolean>);
  },
  staleTime: 5 * 60 * 1000,
});

Best Practices

  1. Server-side validation: Always validate permissions on the server
  2. Fail securely: Default to denying access if unsure
  3. Audit logging: Log all access control decisions
  4. Clear error messages: Provide helpful denial reasons
  5. Test thoroughly: Test all permission combinations
  6. Document policies: Keep clear documentation of access rules
  7. Use constants: Define permission strings as constants
export const PERMISSIONS = {
  POSTS_CREATE: "posts:create",
  POSTS_EDIT: "posts:edit",
  POSTS_DELETE: "posts:delete",
  POSTS_LIST: "posts:list",
  POSTS_SHOW: "posts:show",
} as const;

Further Reading

Build docs developers (and LLMs) love