Skip to main content
Version: 4.xx.xx

Access Control

Introduction

Access control is a broad topic where there are lots of advanced solutions that provide a different sets of features. Refine is deliberately agnostic for its own API to be able to integrate different methods (RBAC, ABAC, ACL, etc.) and different libraries (Casbin, CASL, Cerbos, AccessControl.js). can method would be the entry point for those solutions.

Refer to the Access Control Provider documentation for detailed information.

Refine provides an agnostic API via the accessControlProvider to manage access control throughout your app.

An accessControlProvider must implement only one async method named can to be used to check if the desired access will be granted.

We will be using Casbin in this guide for users with different roles who have different access rights for parts of the app.

Installation

We need to install Casbin.

npm i casbin
CAUTION

To make this example more visual, we used the @refinedev/antd package. If you are using Refine headless, you need to provide the components, hooks, or helpers imported from the @refinedev/antd package.

Setup

The app will have three resources: posts, users, and categories with CRUD pages(list, create, edit, and show).

You can refer to CodeSandbox to see how they are implemented

App.tsx will look like this before we begin implementing access control:

src/App.tsx
import { Refine } from "@refinedev/core";
import { ThemedLayoutV2, ErrorComponent, RefineThemes } from "@refinedev/antd";
import dataProvider from "@refinedev/simple-rest";
import routerProvider from "@refinedev/react-router-v6";

import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom";

import { ConfigProvider } from "antd";
import "@refinedev/antd/dist/reset.css";

const API_URL = "https://api.fake-rest.refine.dev";

const App: React.FC = () => {
return (
<BrowserRouter>
<ConfigProvider theme={RefineThemes.Blue}>
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider(API_URL)}
resources={[
{
name: "posts",
list: "/posts",
create: "/posts/create",
edit: "/posts/edit/:id",
show: "/posts/show/:id",
meta: {
canDelete: true,
},
},
{
name: "users",
list: "/users",
create: "/users/create",
edit: "/users/edit/:id",
show: "/users/show/:id",
},
{
name: "categories",
list: "/categories",
create: "/categories/create",
edit: "/categories/edit/:id",
show: "/categories/show/:id",
},
]}
>
<Routes>
<Route
element={
<ThemedLayoutV2>
<Outlet />
</ThemedLayoutV2>
}
>
<Route path="posts">
<Route index element={<PostList />} />
<Route path="create" element={<PostCreate />} />
<Route path="show/:id" element={<PostShow />} />
<Route path="edit/:id" element={<PostEdit />} />
</Route>
<Route path="users">
<Route index element={<UserList />} />
<Route path="create" element={<UserCreate />} />
<Route path="show/:id" element={<UserShow />} />
<Route path="edit/:id" element={<UserEdit />} />
</Route>
<Route path="categories">
<Route index element={<CategoryList />} />
<Route path="create" element={<CategoryCreate />} />
<Route path="show/:id" element={<CategoryShow />} />
<Route path="edit/:id" element={<CategoryEdit />} />
</Route>
</Route>
<Route path="*" element={<ErrorComponent />} />
</Routes>
</Refine>
</ConfigProvider>
</BrowserRouter>
);
};

export default App;

Adding Policy and Model

The way Casbin works is that access rights are checked according to policies that are defined based on a model. You can find further information about how models and policies work here.

Let's add a model and a policy for a role editor that have list access for posts resource.

src/accessControl.ts
import { newModel, StringAdapter } from "casbin";

export const model = newModel(`
[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) && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)
`);

export const adapter = new MemoryAdapter(`
p, editor, posts, list
`);
TIP

You can can find more examples in Casbin documentation or play with lots of examples in Casbin editor

Adding accessControlProvider

Now we will implement the can method for accessControlProvider to integrate our policy.

src/App.tsx
// ...
import { newEnforcer } from "casbin";

import { model, adapter } from "./accessControl";

const App: React.FC = () => {
return (
<BrowserRouter>
<Refine
accessControlProvider={{
can: async ({ resource, action }) => {
const enforcer = await newEnforcer(model, adapter);
const can = await enforcer.enforce("editor", resource, action);

return { can };
},
}}
//...
>
{/* ... */}
</Refine>
</BrowserRouter>
);
};

export default App;

Whenever a part of the app checks for access control, Refine passes resource, action, and params parameters to can and then we can use these parameters to integrate our specific access control solution which is Casbin in this case.

Our model provides that user with role editor have access for list action on posts resource. Even though we have two other resources, since our policy doesn't include them, they will not appear on the sidebar menu. Also in the list page of posts, buttons for create, edit and show buttons will be disabled since they are not included in the policy.

localhost:5173/posts

Adding Different Roles

We can provide different access rights to a different types of users for different parts of the app. We can do that by adding policies for the different roles.

export const adapter = new MemoryAdapter(`
p, admin, posts, (list)|(create)
p, admin, users, (list)|(create)
p, admin, categories, (list)|(create)

p, editor, posts, (list)|(create)
p, editor, categories, list
`);
  • admin will have access to list and create for every resource
  • editor will have access to list and create for posts
  • editor won't have any access for users
  • editor will have only list access for categories

We can demonstrate the effect of different roles by changing the role dynamically. Let's implement a switch in the header for selecting either admin or editor role to see the effect on the app.

src/App.tsx
import { Header } from "components/header";

const App: React.FC = () => {
import { CanAccess } from "@refinedev/core";
const role = localStorage.getItem("role") ?? "admin";

return (
<BrowserRouter>
<Refine
accessControlProvider={{
can: async ({ resource, action }) => {
const enforcer = await newEnforcer(model, adapter);
const can = await enforcer.enforce(role, resource, action);

return {
can,
};
},
}}
//...
>
<Routes>
<Route
element={
<ThemedLayoutV2 Header={() => <Header role={role} />}>
<CanAccess>
<Outlet />
</CanAccess>
</ThemedLayoutV2>
}
>
{/* ... */}
</Route>
</Routes>
{/* ... */}
</Refine>
</BrowserRouter>
);
};

export default App;
Header Component
src/components/header.tsx
import { Layout, Radio } from "antd";

interface HeaderProps {
role: string;
}

export const Header: React.FC<HeaderProps> = ({ role }) => {
return (
<Layout.Header
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "48px",
backgroundColor: "#FFF",
}}
>
<Radio.Group
value={role}
onChange={(event) => {
localStorage.setItem("role", event.target.value);
location.reload();
}}
>
<Radio.Button value="admin">Admin</Radio.Button>
<Radio.Button value="editor">Editor</Radio.Button>
</Radio.Group>
</Layout.Header>
);
};

Now, let's see how the application will appear when logging in as an admin or editor.

localhost:5173

Handling access with params

ID Based Access

Let's update our policies to handle id based access control points like edit, show pages, and delete button.

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

p, admin, users, (list)|(create)
p, admin, users/*, (edit)|(show)|(delete)

p, admin, categories, (list)|(create)
p, admin, categories/*, (edit)|(show)|(delete)

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

p, editor, categories, list
`);
  • admin will have edit, show and delete access for every resource
  • editor will have edit and show access for posts
TIP

* is a wildcard. Specific ids can be targeted too. For example If you want editor role to have delete access for post with id 5, you can add this policy:

export const adapter = new MemoryAdapter(`
p, editor, posts/5, delete
`);

We must handle id based access controls in the can method. id parameter will be accessible in params.

src/App.tsx
const App: React.FC = () => {
return (
<Refine
//...
accessControlProvider={{
can: async ({ resource, action, params }) => {
const enforcer = await newEnforcer(model, adapter);

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

return { can };
}

const can = await enforcer.enforce(role, resource, action);

return { can };
},
}}
>
{/* ... */}
</Refine>
);
};

export default App;

Field Based Access

We can also check access control for specific areas in our app like a certain field of a table. This can be achieved by adding a special action for the custom access control point in our policies.

For example, we may want to deny editor roles to access hit field in the posts resource without denying the admin role. This can be done with RBAC with deny-override 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)
`);

export const adapter = new MemoryAdapter(`
p, admin, posts, (list)|(create)
p, admin, posts/*, (edit)|(show)|(delete)
p, admin, posts/*, field

p, admin, users, (list)|(create)
p, admin, users/*, (edit)|(show)|(delete)

p, admin, categories, (list)|(create)
p, admin, categories/*, (edit)|(show)|(delete)

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

p, editor, categories, list
`);
  • admin have field access for every field of posts
  • editor won't have field access for hit field of posts

Then we must handle the field action in the can method:

src/App.tsx
const App: React.FC = () => {
return (
<Refine
//...
accessControlProvider={{
can: async ({ resource, action, params }) => {
const enforcer = await newEnforcer(model, adapter);

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

return { can };
}

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

const can = await enforcer.enforce(role, resource, action);

return { can };
},
}}
>
{/* ... */}
</Refine>
);
};

export default App;

Then it can be used with useCan in the related area:

src/pages/posts/list.tsx
import {
// ...
useCan,
} from "@refinedev/core";

export const PostList: React.FC = () => {
const { data: canAccess } = useCan({
resource: "posts",
action: "field",
params: { field: "hit" },
});

return (
<List>
<Table {...tableProps} rowKey="id">
{canAccess?.can && (
<Table.Column
dataIndex="hit"
title="Hit"
render={(value: number) => (
<NumberField value={value} options={{ notation: "compact" }} />
)}
/>
)}
</Table>
</List>
);
};
TIP

<CanAccess /> can be used too to check access control in custom places in your app.


Now, let's see how the application will appear when logging in as an admin or editor.

localhost:5173

Example

Casbin

Run on your local
npm create refine-app@latest -- --example access-control-casbin

Cerbos

Run on your local
npm create refine-app@latest -- --example access-control-cerbos