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 install casbin
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:
import { Refine } from "@refinedev/core";
import {
ThemedLayoutV2,
notificationProvider,
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.
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
`);
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.
// ...
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.
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.
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
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
.
- admin
- editor
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
*
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
.
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:
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:
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>
);
};
<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
.
- admin
- editor
Example
Casbin
npm create refine-app@latest -- --example access-control-casbin
Cerbos
npm create refine-app@latest -- --example access-control-cerbos