Access Control Provider
Overview
Access control is a broad topic with lots of advanced solutions that provide different sets of features.
refine provides an agnostic API via the accessControlProvider
to manage access control throughout your app, which allows you to integrate different methods, such as RBAC
, ABAC
, ACL
, etc., and libraries, such as Casbin, CASL, Cerbos and AccessControl.js.
To check if a desired access will be granted, the accessControlProvider
should at least have an asynchronous method named can
with the following interface:
type CanParams = {
resource: string;
action: string;
params?: {
resource?: IResourceItem;
id?: BaseKey;
[key: string]: any;
};
};
type CanReturnType = {
can: boolean;
reason?: string;
};
export interface IAccessControlContext {
can?: ({ resource, action, params }: CanParams) => Promise<CanReturnType>;
options?: {
buttons?: {
enableAccessControl?: boolean;
hideIfUnauthorized?: boolean;
};
};
}
const accessControlProvider: IAccessControlContext = {
can: async ({
resource,
action,
params,
}: CanParams): Promise<CanReturnType> => {
return { can: true };
},
options: {
buttons: {
enableAccessControl: true,
hideIfUnauthorized: false,
},
},
};
It's also possible to globally configure buttons' behaviour by passing options
to the accessControlProvider
.
You can still change the behaviour of the buttons independently, however, if no configuration is found, buttons will fallback to configuration defined in options.buttons
.
By default, enableAccessControl
is true and hideIfUnauthorized
is false.
export interface IAccessControlContext {
can?: ({ resource, action, params }: CanParams) => Promise<CanReturnType>;
options?: {
buttons?: {
// default is true
enableAccessControl?: boolean;
// default is false
hideIfUnauthorized?: boolean;
};
};
}
const accessControlProvider: IAccessControlContext = {
can: async ({
resource,
action,
params,
}: CanParams): Promise<CanReturnType> => {
return { can: true };
},
// Global settings
options: {
buttons: {
enableAccessControl: true,
// hide action buttons if not authorized.
hideIfUnauthorized: true,
},
},
};
For more information, refer to these sections in the Interface References documentation
IResourceItem
,BaseKey
,CanParams
,CanReturnType
A basic usage would be like this:
const App: React.FC = () => {
return (
<Refine
// other providers and props
accessControlProvider={{
can: async ({ resource, action, params }) => {
if (resource === "posts" && action === "edit") {
return {
can: false,
reason: "Unauthorized",
};
}
return { can: true };
},
options: {
buttons: {
enableAccessControl: true,
hideIfUnauthorized: false,
},
},
}}
>
{/* your app */}
</Refine>
);
};
Depending on your router, providing accessControlProvider
to the <Refine>
component won't enforce access control by itself; you may need to wrap protected routes with the <CanAccess>
component.
Refer to one of the following documentations, depending on your preferred router:
You can also access the resource object directly.
In the example below, the can
function receives the resource
(ResourceItemProps) object you pass to the <Refine/>
component, which allows you to use Attribute Based Access Control (ABAC), which allows you to grant permissions based on the value of a field in the resource object.
export const accessControlProvider = {
can: async ({ resource, action, params }) => {
const resourceName = params?.resource?.name;
const anyUsefulMeta = params?.resource?.meta?.yourUsefulMeta;
if (
resourceName === "posts" &&
anyUsefulMeta === true &&
action === "edit"
) {
return {
can: false,
reason: "Unauthorized",
};
}
},
};
You can pass a reason
along with can
. It will be accessible using useCan
. It will be shown at the tooltip of the buttons from refine when they are disabled.
You can find access control examples made with refine
- Casbin → Source Code - Demo
- Cerbos → Source Code - Demo
refine checks for access control in its related components and pages.
Hooks and Components
refine provides a hook and a component to use the can
method from the accessControlProvider
.
useCan
useCan
uses the can
for the query function for react-query's useQuery
. It takes the parameters that can
takes, can be configured with queryOptions
of useQuery
and returns the result of useQuery
.
const { data } = useCan({
resource: "resource-you-ask-for-access",
action: "action-type-on-resource",
params: { foo: "optional-params" },
});
const useCan: ({
action,
resource,
params,
queryOptions,
}: CanParams* & {
queryOptions?: UseQueryOptions<CanReturnType>;
}) => UseQueryResult<CanReturnType*>
For more information, refer to these sections in the Interfaces documentation:
CanParams
andCanReturnType
<CanAccess />
<CanAccess />
is a wrapper component that uses useCan
to check for access control. It takes the parameters that can
method takes and also a fallback
. If access control returns true, it renders its children; otherwise, it renders fallback
, if it was provided.
<CanAccess
resource="posts"
action="edit"
params={{ id: 1 }}
fallback={<CustomFallback />}
>
<YourComponent />
</CanAccess>
Performance
As the number of points that check for access control in your app increases, the performance of your app may take a hit, especially if its access control involves remote endpoints. Caching the access control checks helps quite a lot, which can be done easily by configuring the staleTime
and cacheTime
properties since refine uses react-query.
// inside your component
const { data } = useCan({
resource: "resource-you-ask-for-access",
action: "action-type-on-resource",
params: { foo: "optional-params" } },
queryOptions: {
staleTime: 5 * 60 * 1000, // 5 minutes
}
);
By default, refine uses 5 minutes for cacheTime
and 0 minutes for staleTime
for its own access control points.
List of Default Access Control Points
Here is a list of components and pages refine checks for access control:
Sider
Sider is integrated, which means that unaccessible resources won't appear in the sider menu.
Menu items will check access control with { resource, action: "list" }
. For example, if your app has a resource called posts
, it will be checked with { resource: "posts", action: "list" }
.
Buttons
These buttons will be checked for access control.
Let's say they are rendered where resource
is posts
and id
is 1
where applicable. The can
function will receive the resource
(ResourceItemProps) object you passed to the <Refine/>
component, which allows you to use Attribute Based Access Control (ABAC), which allows you to grant permissions based on the value of a field in the resource object.
- List:
{ resource: "posts", action: "list", params: { *resource } }
- Create:
{ resource: "posts", action: "create", params: { *resource } }
- Clone:
{ resource: "posts", action: "create", params: { id: 1, *resource } }
- Edit:
{ resource: "posts", action: "edit", params: { id: 1, *resource } }
- Delete:
{ resource: "posts, action: "delete", params: { id: 1, *resource } }
- Show:
{ resource: "posts", action: "show", params: { id: 1, *resource } }
These buttons will be disabled if access control returns { can: false }
Examples
This example is for Casbin access control provider. You can check our other access control provider, Cerbos as well.
npm create refine-app@latest -- --example access-control-casbin