diff --git a/packages/core/admin/admin/src/features/Auth.tsx b/packages/core/admin/admin/src/features/Auth.tsx index cb94f4b893..8b56270e13 100644 --- a/packages/core/admin/admin/src/features/Auth.tsx +++ b/packages/core/admin/admin/src/features/Auth.tsx @@ -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; 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; diff --git a/packages/core/admin/admin/src/hooks/useRBAC.ts b/packages/core/admin/admin/src/hooks/useRBAC.ts index 8b3468763d..33172b67ac 100644 --- a/packages/core/admin/admin/src/hooks/useRBAC.ts +++ b/packages/core/admin/admin/src/hooks/useRBAC.ts @@ -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; */ const useRBAC = ( permissionsToCheck: Record | 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(); - const [data, setData] = useState>(); + const [isLoading, setIsLoading] = React.useState(true); + const [error, setError] = React.useState(); + const [data, setData] = React.useState>(); - 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>((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, ]); /** diff --git a/packages/core/content-manager/admin/src/features/DocumentRBAC.tsx b/packages/core/content-manager/admin/src/features/DocumentRBAC.tsx index 0369415798..dd95993e45 100644 --- a/packages/core/content-manager/admin/src/features/DocumentRBAC.tsx +++ b/packages/core/content-manager/admin/src/features/DocumentRBAC.tsx @@ -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 diff --git a/packages/core/content-manager/admin/src/hooks/useDocument.ts b/packages/core/content-manager/admin/src/hooks/useDocument.ts index 0144d20dff..878338fd24 100644 --- a/packages/core/content-manager/admin/src/hooks/useDocument.ts +++ b/packages/core/content-manager/admin/src/hooks/useDocument.ts @@ -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, }; }; diff --git a/packages/core/content-manager/admin/src/pages/EditView/components/InputRenderer.tsx b/packages/core/content-manager/admin/src/pages/EditView/components/InputRenderer.tsx index bd1f452311..4fd6a11c54 100644 --- a/packages/core/content-manager/admin/src/pages/EditView/components/InputRenderer.tsx +++ b/packages/core/content-manager/admin/src/pages/EditView/components/InputRenderer.tsx @@ -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; * 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.