documentation/versioned_docs/version-3.xx.xx/advanced-tutorials/access-control.md
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.
We need to install Casbin.
<InstallPackagesCommand args="casbin"/>:::caution
To make this example more visual, we used the @pankod/refine-antd package. If you are using Refine headless, you need to provide the components, hooks, or helpers imported from the @pankod/refine-antd package.
:::
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 "@pankod/refine-core";
import {
Layout,
ReadyPage,
useNotificationProvider,
ErrorComponent,
} from "@pankod/refine-antd";
import dataProvider from "@pankod/refine-simple-rest";
import routerProvider from "@pankod/refine-react-router-v6";
import "@pankod/refine-antd/dist/reset.css";
import { PostList, PostCreate, PostEdit, PostShow } from "pages/posts";
import { UserList, UserCreate, UserEdit, UserShow } from "pages/users";
import {
CategoryList,
CategoryCreate,
CategoryEdit,
CategoryShow,
} from "pages/categories";
const API_URL = "https://api.fake-rest.refine.dev";
const App: React.FC = () => {
return (
<Refine
routerProvider={routerProvider}
dataProvider={dataProvider(API_URL)}
Layout={Layout}
ReadyPage={ReadyPage}
notificationProvider={useNotificationProvider}
catchAll={<ErrorComponent />}
resources={[
{
name: "posts",
list: PostList,
create: PostCreate,
edit: PostEdit,
show: PostShow,
canDelete: true,
},
{
name: "users",
list: UserList,
create: UserCreate,
edit: UserEdit,
show: UserShow,
},
{
name: "categories",
list: CategoryList,
create: CategoryCreate,
edit: CategoryEdit,
show: CategoryShow,
},
]}
/>
);
};
export default App;
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
`);
:::tip You can can find more examples in Casbin documentation or play with lots of examples in Casbin editor :::
accessControlProviderNow we will implement the can method for accessControlProvider to integrate our policy.
// ...
// highlight-next-line
import { newEnforcer } from "casbin";
// highlight-next-line
import { model, adapter } from "./accessControl";
const App: React.FC = () => {
return (
<Refine
// ...
// highlight-start
accessControlProvider={{
can: async ({ resource, action }) => {
const enforcer = await newEnforcer(model, adapter);
const can = await enforcer.enforce("editor", resource, action);
return Promise.resolve({ can });
},
}}
// highlight-end
// ...
/>
);
};
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.
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
`);
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.
// ...
// highlight-next-line
import { Header } from "components/header";
const App: React.FC = () => {
// highlight-next-line
const [role, setRole] = useState("admin");
return (
<Refine
// ...
accessControlProvider={{
can: async ({ resource, action }) => {
const enforcer = await newEnforcer(model, adapter);
// highlight-next-line
const can = await enforcer.enforce(role, resource, action);
return Promise.resolve({
can,
});
},
}}
// highlight-next-line
Header={() => <Header role={role} setRole={setRole} />}
// ...
/>
);
};
export default App;
import { AntdLayout, Radio } from "@pankod/refine-antd";
interface HeaderProps {
role: string;
setRole: React.Dispatch<React.SetStateAction<string>>;
}
export const Header: React.FC<HeaderProps> = ({ role, setRole }) => {
return (
<AntdLayout.Header
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "48px",
backgroundColor: "#FFF",
}}
>
<Radio.Group
value={role}
onChange={(event) => setRole(event.target.value)}
>
<Radio.Button value="admin">Admin</Radio.Button>
<Radio.Button value="editor">Editor</Radio.Button>
</Radio.Group>
</AntdLayout.Header>
);
};
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)
// highlight-next-line
p, admin, posts/*, (edit)|(show)|(delete)
p, admin, users, (list)|(create)
// highlight-next-line
p, admin, users/*, (edit)|(show)|(delete)
p, admin, categories, (list)|(create)
// highlight-next-line
p, admin, categories/*, (edit)|(show)|(delete)
p, editor, posts, (list)|(create)
// highlight-next-line
p, editor, posts/*, (edit)|(show)
p, editor, categories, list
`);
:::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.
// ...
const App: React.FC = () => {
// ...
return (
<Refine
// ...
accessControlProvider={{
// highlight-start
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 Promise.resolve({ can });
}
// highlight-end
const can = await enforcer.enforce(role, resource, action);
return Promise.resolve({ can });
},
}}
// ...
/>
);
};
export default App;
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]
// highlight-next-line
p = sub, obj, act, eft
[role_definition]
g = _, _
[policy_effect]
// highlight-next-line
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)
// highlight-next-line
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)
// highlight-next-line
p, editor, posts/hit, field, deny
p, editor, categories, list
`);
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 Promise.resolve({ can });
}
// highlight-start
if (action === "field") {
const can = await enforcer.enforce(
role,
`${resource}/${params?.field}`,
action,
);
return Promise.resolve({ can });
}
// highlight-end
const can = await enforcer.enforce(role, resource, action);
return Promise.resolve({ can });
},
}}
// ...
/>
);
};
export default App;
Then it can be used with useCan in the related area:
import {
// ...
useCan,
} from "@pankod/refine-core";
export const PostList: React.FC = () => {
// ...
// highlight-start
const { data: canAccess } = useCan({
resource: "posts",
action: "field",
params: { field: "hit" },
});
// highlight-end
return (
<List>
<Table {...tableProps} rowKey="id">
// ... // highlight-start
{canAccess?.can && (
<Table.Column
dataIndex="hit"
title="Hit"
render={(value: number) => (
<NumberField value={value} options={{ notation: "compact" }} />
)}
/>
)}
// highlight-end // ...
</Table>
</List>
);
};
:::tip
<CanAccess /> can be used too to check access control in custom places in your app.
:::