Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access control: pessimistic rendering in CRUD views #10258

Merged
merged 14 commits into from
Oct 4, 2024
Merged
1 change: 1 addition & 0 deletions packages/ra-core/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './useCanAccessCallback';
export * from './useCheckAuth';
export * from './useGetIdentity';
export * from './useHandleAuthCallback';
export * from './useIsAuthPending';
export * from './useRequireAccess';
export * from './addRefreshAuthToAuthProvider';
export * from './addRefreshAuthToDataProvider';
Expand Down
43 changes: 43 additions & 0 deletions packages/ra-core/src/auth/useIsAuthPending.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useQueryClient } from '@tanstack/react-query';
import { useResourceContext } from '../core';
import { HintedString } from '../types';
import useAuthProvider from './useAuthProvider';

/**
* A hook that returns true if the authProvider is currently checking the authentication status or the user's access rights.
* @param params
* @param params.action The action to check access for
* @param params.resource The resource to check access for (optional). Defaults to the resource of the current ResourceContext.
* @returns {boolean} true if the authProvider is currently checking the authentication status or the user's access rights, false otherwise.
*/
export const useIsAuthPending = (params: UseIsAuthPendingParams) => {
const { action, ...props } = params;
const queryClient = useQueryClient();
const authProvider = useAuthProvider();
const resource = useResourceContext(props);

if (!authProvider) {
return false;
}

const authQueryState = queryClient.getQueryState(['auth', 'checkAuth', {}]);
const canAccessQueryState = queryClient.getQueryState([
'auth',
'canAccess',
{ action, resource },
]);

if (
authQueryState?.status === 'pending' ||
(authProvider.canAccess && canAccessQueryState?.status === 'pending')
) {
return true;
}

return false;
};

export type UseIsAuthPendingParams = {
resource?: string;
action: HintedString<'list' | 'create' | 'edit' | 'show' | 'delete'>;
};
52 changes: 35 additions & 17 deletions packages/ra-core/src/controller/create/CreateBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
} from './useCreateController';
import { CreateContextProvider } from './CreateContextProvider';
import { Identifier, RaRecord } from '../../types';
import { ResourceContextProvider } from '../../core';
import { OptionalResourceContextProvider } from '../../core';
import { useIsAuthPending } from '../../auth';

/**
* Call useCreateController and put the value in a CreateContext
Expand Down Expand Up @@ -40,28 +41,45 @@ import { ResourceContextProvider } from '../../core';
export const CreateBase = <
RecordType extends Omit<RaRecord, 'id'> = any,
ResultRecordType extends RaRecord = RecordType & { id: Identifier },
MutationOptionsError = Error,
>({
children,
loading = null,
...props
}: CreateControllerProps<RecordType, Error, ResultRecordType> & {
children: ReactNode;
}) => {
}: CreateBaseProps<RecordType, ResultRecordType, MutationOptionsError>) => {
const controllerProps = useCreateController<
RecordType,
Error,
MutationOptionsError,
ResultRecordType
>(props);
const body = (
<CreateContextProvider value={controllerProps}>
{children}
</CreateContextProvider>
);
return props.resource ? (
// support resource override via props
<ResourceContextProvider value={props.resource}>
{body}
</ResourceContextProvider>
) : (
body

const isAuthPending = useIsAuthPending({
resource: controllerProps.resource,
action: 'create',
});

if (isAuthPending && !props.disableAuthentication) {
return loading;
}

return (
<OptionalResourceContextProvider value={controllerProps.resource}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
<OptionalResourceContextProvider value={controllerProps.resource}>
<OptionalResourceContextProvider value={props.resource}>

Otherwise it's a BC. Same for other controllers.

Copy link
Collaborator Author

@djhi djhi Oct 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No because the controller already handles it

<CreateContextProvider value={controllerProps}>
{children}
</CreateContextProvider>
</OptionalResourceContextProvider>
);
};

export interface CreateBaseProps<
RecordType extends Omit<RaRecord, 'id'> = any,
ResultRecordType extends RaRecord = RecordType & { id: Identifier },
MutationOptionsError = Error,
> extends CreateControllerProps<
RecordType,
MutationOptionsError,
ResultRecordType
> {
children: ReactNode;
loading?: ReactNode;
}
40 changes: 26 additions & 14 deletions packages/ra-core/src/controller/edit/EditBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { ReactNode } from 'react';
import { RaRecord } from '../../types';
import { useEditController, EditControllerProps } from './useEditController';
import { EditContextProvider } from './EditContextProvider';
import { ResourceContextProvider } from '../../core';
import { OptionalResourceContextProvider } from '../../core';
import { useIsAuthPending } from '../../auth';

/**
* Call useEditController and put the value in a EditContext
Expand Down Expand Up @@ -37,20 +38,31 @@ import { ResourceContextProvider } from '../../core';
*/
export const EditBase = <RecordType extends RaRecord = any>({
children,
loading = null,
...props
}: { children: ReactNode } & EditControllerProps<RecordType>) => {
}: EditBaseProps<RecordType>) => {
const controllerProps = useEditController<RecordType>(props);
const body = (
<EditContextProvider value={controllerProps}>
{children}
</EditContextProvider>
);
return props.resource ? (
// support resource override via props
<ResourceContextProvider value={props.resource}>
{body}
</ResourceContextProvider>
) : (
body

const isAuthPending = useIsAuthPending({
resource: controllerProps.resource,
action: 'edit',
});

if (isAuthPending && !props.disableAuthentication) {
return loading;
}

return (
<OptionalResourceContextProvider value={controllerProps.resource}>
<EditContextProvider value={controllerProps}>
{children}
</EditContextProvider>
</OptionalResourceContextProvider>
);
};

export interface EditBaseProps<RecordType extends RaRecord = RaRecord>
extends EditControllerProps<RecordType> {
children: ReactNode;
loading?: ReactNode;
}
25 changes: 21 additions & 4 deletions packages/ra-core/src/controller/list/InfiniteListBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {
useInfiniteListController,
InfiniteListControllerProps,
} from './useInfiniteListController';
import { ResourceContextProvider } from '../../core';
import { OptionalResourceContextProvider } from '../../core';
import { RaRecord } from '../../types';
import { ListContextProvider } from './ListContextProvider';
import { InfinitePaginationContext } from './InfinitePaginationContext';
import { useIsAuthPending } from '../../auth';

/**
* Call useInfiniteListController and put the value in a ListContext
Expand Down Expand Up @@ -45,11 +46,21 @@ import { InfinitePaginationContext } from './InfinitePaginationContext';
*/
export const InfiniteListBase = <RecordType extends RaRecord = any>({
children,
loading = null,
...props
}: InfiniteListControllerProps<RecordType> & { children: ReactNode }) => {
}: InfiniteListBaseProps<RecordType>) => {
const controllerProps = useInfiniteListController<RecordType>(props);
const isAuthPending = useIsAuthPending({
resource: controllerProps.resource,
action: 'list',
});

if (isAuthPending && !props.disableAuthentication) {
return loading;
}

return (
<ResourceContextProvider value={props.resource}>
<OptionalResourceContextProvider value={controllerProps.resource}>
<ListContextProvider value={controllerProps}>
<InfinitePaginationContext.Provider
value={{
Expand All @@ -65,6 +76,12 @@ export const InfiniteListBase = <RecordType extends RaRecord = any>({
{children}
</InfinitePaginationContext.Provider>
</ListContextProvider>
</ResourceContextProvider>
</OptionalResourceContextProvider>
);
};

export interface InfiniteListBaseProps<RecordType extends RaRecord = any>
extends InfiniteListControllerProps<RecordType> {
children: ReactNode;
loading?: ReactNode;
}
36 changes: 28 additions & 8 deletions packages/ra-core/src/controller/list/ListBase.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from 'react';
import { ReactNode } from 'react';
import { useListController, ListControllerProps } from './useListController';
import { ResourceContextProvider } from '../../core';
import { OptionalResourceContextProvider } from '../../core';
import { RaRecord } from '../../types';
import { ListContextProvider } from './ListContextProvider';
import { useIsAuthPending } from '../../auth';

/**
* Call useListController and put the value in a ListContext
Expand Down Expand Up @@ -41,11 +42,30 @@ import { ListContextProvider } from './ListContextProvider';
*/
export const ListBase = <RecordType extends RaRecord = any>({
children,
loading = null,
...props
}: ListControllerProps<RecordType> & { children: ReactNode }) => (
<ResourceContextProvider value={props.resource}>
<ListContextProvider value={useListController<RecordType>(props)}>
{children}
</ListContextProvider>
</ResourceContextProvider>
);
}: ListBaseProps<RecordType>) => {
const controllerProps = useListController<RecordType>(props);
const isAuthPending = useIsAuthPending({
resource: controllerProps.resource,
action: 'list',
});

if (isAuthPending && !props.disableAuthentication) {
return loading;
}

return (
<OptionalResourceContextProvider value={controllerProps.resource}>
<ListContextProvider value={controllerProps}>
{children}
</ListContextProvider>
</OptionalResourceContextProvider>
);
};

export interface ListBaseProps<RecordType extends RaRecord = any>
extends ListControllerProps<RecordType> {
children: ReactNode;
loading?: ReactNode;
}
40 changes: 26 additions & 14 deletions packages/ra-core/src/controller/show/ShowBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import * as React from 'react';
import { RaRecord } from '../../types';
import { useShowController, ShowControllerProps } from './useShowController';
import { ShowContextProvider } from './ShowContextProvider';
import { ResourceContextProvider } from '../../core';
import { OptionalResourceContextProvider } from '../../core';
import { useIsAuthPending } from '../../auth';

/**
* Call useShowController and put the value in a ShowContext
Expand Down Expand Up @@ -36,20 +37,31 @@ import { ResourceContextProvider } from '../../core';
*/
export const ShowBase = <RecordType extends RaRecord = any>({
children,
loading = null,
...props
}: { children: React.ReactNode } & ShowControllerProps<RecordType>) => {
}: ShowBaseProps<RecordType>) => {
const controllerProps = useShowController<RecordType>(props);
const body = (
<ShowContextProvider value={controllerProps}>
{children}
</ShowContextProvider>
);
return props.resource ? (
// support resource override via props
<ResourceContextProvider value={props.resource}>
{body}
</ResourceContextProvider>
) : (
body

const isAuthPending = useIsAuthPending({
resource: controllerProps.resource,
action: 'show',
});

if (isAuthPending && !props.disableAuthentication) {
return loading;
}

return (
<OptionalResourceContextProvider value={controllerProps.resource}>
<ShowContextProvider value={controllerProps}>
{children}
</ShowContextProvider>
</OptionalResourceContextProvider>
);
};

export interface ShowBaseProps<RecordType extends RaRecord = RaRecord>
extends ShowControllerProps<RecordType> {
children: React.ReactNode;
loading?: React.ReactNode;
}
4 changes: 4 additions & 0 deletions packages/ra-ui-materialui/src/detail/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from 'ra-core';

import { CreateView, CreateViewProps } from './CreateView';
import { Loading } from '../layout';

/**
* Page component for the Create view
Expand Down Expand Up @@ -81,6 +82,7 @@ export const Create = <
disableAuthentication={disableAuthentication}
hasEdit={hasEdit}
hasShow={hasShow}
loading={defaultLoading}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<Create> should allow overriding the loading component. Same for the others.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stupid me

>
<CreateView {...rest} />
</CreateBase>
Expand All @@ -97,3 +99,5 @@ export interface CreateProps<
ResultRecordType
>,
CreateViewProps {}

const defaultLoading = <Loading />;
4 changes: 4 additions & 0 deletions packages/ra-ui-materialui/src/detail/Edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
EditControllerProps,
} from 'ra-core';
import { EditView, EditViewProps } from './EditView';
import { Loading } from '../layout';

/**
* Page component for the Edit view
Expand Down Expand Up @@ -77,6 +78,7 @@ export const Edit = <RecordType extends RaRecord = any>(
redirect={redirect}
transform={transform}
disableAuthentication={disableAuthentication}
loading={defaultLoading}
>
<EditView {...rest} />
</EditBase>
Expand All @@ -86,3 +88,5 @@ export const Edit = <RecordType extends RaRecord = any>(
export interface EditProps<RecordType extends RaRecord = any, ErrorType = Error>
extends EditControllerProps<RecordType, ErrorType>,
EditViewProps {}

const defaultLoading = <Loading />;
Loading
Loading