Skip to content

Commit

Permalink
Merge pull request #10258 from marmelab/access-control-views-loading
Browse files Browse the repository at this point in the history
Access control: pessimistic rendering in CRUD views
  • Loading branch information
fzaninotto authored Oct 4, 2024
2 parents 8a8cead + a60d5e6 commit 30924a4
Show file tree
Hide file tree
Showing 25 changed files with 1,275 additions and 448 deletions.
67 changes: 67 additions & 0 deletions packages/ra-core/src/auth/WithPermissions.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from 'react';
import { AuthProvider } from '../types';
import { CoreAdminContext } from '../core';
import { TestMemoryRouter, WithPermissions } from '..';

export default {
title: 'ra-core/auth/WithPermissions',
};

export const NoAuthProvider = () => (
<TestMemoryRouter>
<CoreAdminContext>
<WithPermissions component={StateInspector} />
</CoreAdminContext>
</TestMemoryRouter>
);

export const NoAuthProviderGetPermissions = ({
loading = () => <p>Loading...</p>,
}: {
loading: React.ComponentType;
}) => (
<TestMemoryRouter>
<CoreAdminContext
authProvider={{
login: () => Promise.reject('bad method'),
logout: () => Promise.reject('bad method'),
checkAuth: () =>
new Promise(resolve => setTimeout(resolve, 300)),
checkError: () => Promise.reject('bad method'),
}}
>
<WithPermissions component={StateInspector} loading={loading} />
</CoreAdminContext>
</TestMemoryRouter>
);

export const WithAuthProviderAndGetPermissions = ({
loading = () => <p>Loading...</p>,
authProvider = {
login: () => Promise.reject('bad method'),
logout: () => Promise.reject('bad method'),
checkAuth: () => new Promise(resolve => setTimeout(resolve, 300)),
checkError: () => Promise.reject('bad method'),
getPermissions: () =>
new Promise(resolve => setTimeout(resolve, 300, 'admin')),
},
}: {
loading: React.ComponentType;
authProvider?: AuthProvider;
}) => (
<TestMemoryRouter>
<CoreAdminContext authProvider={authProvider}>
<WithPermissions component={StateInspector} loading={loading} />
</CoreAdminContext>
</TestMemoryRouter>
);

const StateInspector = ({ permissions }: { permissions: any }) => (
<div>
{permissions === 'admin' ? (
<p>Sensitive data</p>
) : (
<p>Non sensitive data</p>
)}
</div>
);
27 changes: 20 additions & 7 deletions packages/ra-core/src/auth/WithPermissions.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as React from 'react';
import { Children, ReactElement, ComponentType, createElement } from 'react';
import { Location } from 'react-router-dom';

Expand All @@ -17,6 +18,7 @@ export interface WithPermissionsProps {
authParams?: object;
children?: WithPermissionsChildren;
component?: ComponentType<any>;
loading?: ComponentType<any>;
location?: Location;
render?: WithPermissionsChildren;
staticContext?: object;
Expand Down Expand Up @@ -60,8 +62,15 @@ const isEmptyChildren = children => Children.count(children) === 0;
* );
*/
const WithPermissions = (props: WithPermissionsProps) => {
const { authParams, children, render, component, staticContext, ...rest } =
props;
const {
authParams,
children,
render,
component,
loading: Loading = null,
staticContext,
...rest
} = props;
warning(
(render && children && !isEmptyChildren(children)) ||
(render && component) ||
Expand All @@ -70,11 +79,15 @@ const WithPermissions = (props: WithPermissionsProps) => {
);

const { isPending: isAuthenticationPending } = useAuthenticated(authParams);
const { permissions, isPending } = usePermissions(authParams, {
enabled: !isAuthenticationPending,
});
if (isPending) {
return null;
const { permissions, isPending: isPendingPermissions } = usePermissions(
authParams,
{
enabled: !isAuthenticationPending,
}
);
// We must check both pending states here as if the authProvider does not implement getPermissions, isPendingPermissions will always be false
if (isAuthenticationPending || isPendingPermissions) {
return Loading ? <Loading /> : null;
}

if (component) {
Expand Down
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'>;
};
Loading

0 comments on commit 30924a4

Please sign in to comment.