Merge pull request #21310 from strapi/fix/i18n-rbac-locale-switch

Use correct user permissions when switching between locales
This commit is contained in:
Alexandre BODIN 2024-09-18 11:24:36 +02:00 committed by GitHub
commit f2ff35145b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 79 additions and 27 deletions

View File

@ -6,6 +6,7 @@ import { Login } from '../../../shared/contracts/authentication';
import { createContext } from '../components/Context';
import { useTypedDispatch, useTypedSelector } from '../core/store/hooks';
import { useStrapiApp } from '../features/StrapiApp';
import { useQueryParams } from '../hooks/useQueryParams';
import { login as loginAction, logout as logoutAction, setLocale } from '../reducer';
import { adminApi } from '../services/api';
import {
@ -45,7 +46,8 @@ interface AuthContextValue {
*/
checkUserHasPermissions: (
permissions?: Permission[],
passedPermissions?: Permission[]
passedPermissions?: Permission[],
rawQueryContext?: string
) => Promise<Permission[]>;
isLoading: boolean;
permissions: Permission[];
@ -80,6 +82,8 @@ const AuthProvider = ({
const dispatch = useTypedDispatch();
const runRbacMiddleware = useStrapiApp('AuthProvider', (state) => state.rbac.run);
const location = useLocation();
const [{ rawQuery }] = useQueryParams();
const token = useTypedSelector((state) => state.admin_app.token ?? null);
const { data: user, isLoading: isLoadingUser } = useGetMeQuery(undefined, {
@ -194,7 +198,20 @@ const AuthProvider = ({
const [checkPermissions] = useLazyCheckPermissionsQuery();
const checkUserHasPermissions: AuthContextValue['checkUserHasPermissions'] = React.useCallback(
async (permissions, passedPermissions) => {
async (
permissions,
passedPermissions,
// TODO:
// Here we have parameterised checkUserHasPermissions in order to pass
// query context from elsewhere in the application.
// See packages/core/content-manager/admin/src/features/DocumentRBAC.tsx
// This is in order to calculate permissions on accurate query params.
// We should be able to rely on the query params in this provider
// If we need to pass additional context to the RBAC middleware
// we should define a better context type.
rawQueryContext
) => {
/**
* If there's no permissions to check, then we allow it to
* pass to preserve existing behaviours.
@ -223,7 +240,7 @@ const AuthProvider = ({
user,
permissions: userPermissions,
pathname: location.pathname,
search: location.search.split('?')[1] ?? '',
search: (rawQueryContext || rawQuery).split('?')[1] ?? '',
},
matchingPermissions
);
@ -249,7 +266,7 @@ const AuthProvider = ({
return middlewaredPermissions.filter((_, index) => data?.data[index] === true);
}
},
[checkPermissions, location.pathname, location.search, runRbacMiddleware, user, userPermissions]
[checkPermissions, location.pathname, rawQuery, runRbacMiddleware, user, userPermissions]
);
const isLoading = isLoadingUser || isLoadingPermissions;

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import * as React from 'react';
import isEqual from 'lodash/isEqual';
@ -43,7 +43,8 @@ type AllowedActions = Record<string, boolean>;
*/
const useRBAC = (
permissionsToCheck: Record<string, Permission[]> | Permission[] = [],
passedPermissions?: Permission[]
passedPermissions?: Permission[],
rawQueryContext?: string
): {
allowedActions: AllowedActions;
isLoading: boolean;
@ -51,13 +52,13 @@ const useRBAC = (
permissions: Permission[];
} => {
const isLoadingAuth = useAuth('useRBAC', (state) => state.isLoading);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<unknown>();
const [data, setData] = useState<Record<string, boolean>>();
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState<unknown>();
const [data, setData] = React.useState<Record<string, boolean>>();
const warnOnce = useMemo(() => once(console.warn), []);
const warnOnce = React.useMemo(() => once(console.warn), []);
const actualPermissionsToCheck: Permission[] = useMemo(() => {
const actualPermissionsToCheck: Permission[] = React.useMemo(() => {
if (Array.isArray(permissionsToCheck)) {
return permissionsToCheck;
} else {
@ -73,7 +74,7 @@ const useRBAC = (
* This is the default value we return until the queryResults[i].data
* are all resolved with data. This preserves the original behaviour.
*/
const defaultAllowedActions = useMemo(() => {
const defaultAllowedActions = React.useMemo(() => {
return actualPermissionsToCheck.reduce<Record<string, boolean>>((acc, permission) => {
return {
...acc,
@ -85,13 +86,19 @@ const useRBAC = (
const checkUserHasPermissions = useAuth('useRBAC', (state) => state.checkUserHasPermissions);
const permssionsChecked = usePrev(actualPermissionsToCheck);
useEffect(() => {
if (!isEqual(permssionsChecked, actualPermissionsToCheck)) {
const contextChecked = usePrev(rawQueryContext);
React.useEffect(() => {
if (
!isEqual(permssionsChecked, actualPermissionsToCheck) ||
// TODO: also run this when the query context changes
contextChecked !== rawQueryContext
) {
setIsLoading(true);
setData(undefined);
setError(undefined);
checkUserHasPermissions(actualPermissionsToCheck, passedPermissions)
checkUserHasPermissions(actualPermissionsToCheck, passedPermissions, rawQueryContext)
.then((res) => {
if (res) {
setData(
@ -117,6 +124,8 @@ const useRBAC = (
passedPermissions,
permissionsToCheck,
permssionsChecked,
contextChecked,
rawQueryContext,
]);
/**

View File

@ -1,6 +1,13 @@
import * as React from 'react';
import { useRBAC, useAuth, type Permission, createContext, Page } from '@strapi/admin/strapi-admin';
import {
useRBAC,
useAuth,
type Permission,
createContext,
Page,
useQueryParams,
} from '@strapi/admin/strapi-admin';
import { useParams } from 'react-router-dom';
import type { Schema } from '@strapi/types';
@ -63,6 +70,7 @@ const DocumentRBAC = ({ children, permissions }: DocumentRBACProps) => {
if (!slug) {
throw new Error('Cannot find the slug param in the URL');
}
const [{ rawQuery }] = useQueryParams<{ plugins?: { i18n?: { locale?: string } } }>();
const userPermissions = useAuth('DocumentRBAC', (state) => state.permissions);
@ -76,7 +84,14 @@ const DocumentRBAC = ({ children, permissions }: DocumentRBACProps) => {
}, {});
}, [slug, userPermissions]);
const { isLoading, allowedActions } = useRBAC(contentTypePermissions, permissions ?? undefined);
const { isLoading, allowedActions } = useRBAC(
contentTypePermissions,
permissions ?? undefined,
// TODO: useRBAC context should be typed and built differently
// We are passing raw query as context to the hook so that it can
// rely on the locale provided from DocumentRBAC for its permission calculations.
rawQuery
);
const canCreateFields =
!isLoading && allowedActions.canCreate

View File

@ -195,16 +195,20 @@ const useDoc = () => {
throw new Error('Could not find model in url params');
}
const document = useDocument(
{ documentId: origin || id, model: slug, collectionType, params },
{
skip: id === 'create' || (!origin && !id && collectionType !== SINGLE_TYPES),
}
);
const returnId = origin || id === 'create' ? undefined : id;
return {
collectionType,
model: slug,
id: origin || id === 'create' ? undefined : id,
...useDocument(
{ documentId: origin || id, model: slug, collectionType, params },
{
skip: id === 'create' || (!origin && !id && collectionType !== SINGLE_TYPES),
}
),
id: returnId,
...document,
};
};

View File

@ -8,6 +8,7 @@ import {
} from '@strapi/admin/strapi-admin';
import { useIntl } from 'react-intl';
import { SINGLE_TYPES } from '../../../constants/collections';
import { useDocumentRBAC } from '../../../features/DocumentRBAC';
import { useDoc } from '../../../hooks/useDocument';
import { useDocLayout } from '../../../hooks/useDocumentLayout';
@ -35,7 +36,7 @@ type InputRendererProps = DistributiveOmit<EditFieldLayout, 'size'>;
* components such as Blocks / Relations.
*/
const InputRenderer = ({ visible, hint: providedHint, ...props }: InputRendererProps) => {
const { id } = useDoc();
const { id, document, collectionType } = useDoc();
const isFormDisabled = useForm('InputRenderer', (state) => state.disabled);
const isInDynamicZone = useDynamicZone('isInDynamicZone', (state) => state.isInDynamicZone);
@ -45,8 +46,14 @@ const InputRenderer = ({ visible, hint: providedHint, ...props }: InputRendererP
const canUpdateFields = useDocumentRBAC('InputRenderer', (rbac) => rbac.canUpdateFields);
const canUserAction = useDocumentRBAC('InputRenderer', (rbac) => rbac.canUserAction);
const editableFields = id ? canUpdateFields : canCreateFields;
const readableFields = id ? canReadFields : canCreateFields;
let idToCheck = id;
if (collectionType === SINGLE_TYPES) {
idToCheck = document?.documentId;
}
const editableFields = idToCheck ? canUpdateFields : canCreateFields;
const readableFields = idToCheck ? canReadFields : canCreateFields;
/**
* Component fields are always readable and editable,
* however the fields within them may not be.