diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md
index ba77f065584cfe..bd35fb87beb9ce 100644
--- a/docs/reference-guides/data/data-core.md
+++ b/docs/reference-guides/data/data-core.md
@@ -550,6 +550,26 @@ _Returns_
- `boolean`: True if the REST request was completed. False otherwise.
+### hasPermission
+
+Returns whether the current user can perform the given action on the entity record.
+
+Calling this may trigger an OPTIONS request to the REST API via the `hasPermission()` resolver.
+
+
+
+_Parameters_
+
+- _state_ `State`: Data state.
+- _action_ `string`: Action to check. One of: 'create', 'read', 'update', 'delete'.
+- _kind_ `string`: Entity kind.
+- _name_ `string`: Entity name
+- _key_ `EntityRecordKey`: Optional record's key.
+
+_Returns_
+
+- `boolean | undefined`: Whether or not the user can perform the action, or `undefined` if the OPTIONS request is still being made.
+
### hasRedo
Returns true if there is a next edit from the current undo offset for the entity records edits history, and false otherwise.
diff --git a/packages/core-data/README.md b/packages/core-data/README.md
index 694f780dafb99d..50ce5957eb2633 100644
--- a/packages/core-data/README.md
+++ b/packages/core-data/README.md
@@ -871,6 +871,26 @@ _Returns_
- `boolean`: True if the REST request was completed. False otherwise.
+### hasPermission
+
+Returns whether the current user can perform the given action on the entity record.
+
+Calling this may trigger an OPTIONS request to the REST API via the `hasPermission()` resolver.
+
+
+
+_Parameters_
+
+- _state_ `State`: Data state.
+- _action_ `string`: Action to check. One of: 'create', 'read', 'update', 'delete'.
+- _kind_ `string`: Entity kind.
+- _name_ `string`: Entity name
+- _key_ `EntityRecordKey`: Optional record's key.
+
+_Returns_
+
+- `boolean | undefined`: Whether or not the user can perform the action, or `undefined` if the OPTIONS request is still being made.
+
### hasRedo
Returns true if there is a next edit from the current undo offset for the entity records edits history, and false otherwise.
diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js
index 5eb6fd8642c75e..d5b4a5db764ee2 100644
--- a/packages/core-data/src/resolvers.js
+++ b/packages/core-data/src/resolvers.js
@@ -424,6 +424,90 @@ export const canUser =
} );
};
+/**
+ * Checks whether the current user can perform the given action on the entity record.
+ *
+ * @param {string} action Action to check. One of: 'create', 'read', 'update', 'delete'.
+ * @param {string} kind Entity kind.
+ * @param {string} name Entity name
+ * @param {number|string} key Optional record's key.
+ */
+export const hasPermission =
+ ( action, kind, name, key ) =>
+ async ( { dispatch, registry } ) => {
+ const configs = await dispatch( getOrLoadEntitiesConfig( kind, name ) );
+ const entityConfig = configs.find(
+ ( config ) => config.name === name && config.kind === kind
+ );
+ if ( ! entityConfig ) {
+ return;
+ }
+
+ const { hasStartedResolution } = registry.select( STORE_NAME );
+ const supportedActions = [ 'create', 'read', 'update', 'delete' ];
+
+ if ( ! supportedActions.includes( action ) ) {
+ throw new Error( `'${ action }' is not a valid action.` );
+ }
+
+ // Prevent resolving the same resource twice.
+ for ( const relatedAction of supportedActions ) {
+ if ( relatedAction === action ) {
+ continue;
+ }
+ const isAlreadyResolving = hasStartedResolution( 'hasPermission', [
+ relatedAction,
+ kind,
+ name,
+ key,
+ ] );
+ if ( isAlreadyResolving ) {
+ return;
+ }
+ }
+
+ let response;
+ try {
+ response = await apiFetch( {
+ path: entityConfig.baseURL + ( key ? '/' + key : '' ),
+ method: 'OPTIONS',
+ parse: false,
+ } );
+ } catch ( error ) {
+ // Do nothing if our OPTIONS request comes back with an API error (4xx or
+ // 5xx). The previously determined isAllowed value will remain in the store.
+ return;
+ }
+
+ // Optional chaining operator is used here because the API requests don't
+ // return the expected result in the native version. Instead, API requests
+ // only return the result, without including response properties like the headers.
+ const allowHeader = response.headers?.get( 'allow' );
+ const allowedMethods = allowHeader?.allow || allowHeader || '';
+
+ const permissions = {};
+ const methods = {
+ create: 'POST',
+ read: 'GET',
+ update: 'PUT',
+ delete: 'DELETE',
+ };
+ for ( const [ actionName, methodName ] of Object.entries( methods ) ) {
+ permissions[ actionName ] = allowedMethods.includes( methodName );
+ }
+
+ registry.batch( () => {
+ for ( const supportedAction of supportedActions ) {
+ dispatch.receiveUserPermission(
+ [ supportedAction, kind, name, key ]
+ .filter( Boolean )
+ .join( '/' ),
+ permissions[ supportedAction ]
+ );
+ }
+ } );
+ };
+
/**
* Checks whether the current user can perform the given action on the given
* REST resource.
diff --git a/packages/core-data/src/selectors.ts b/packages/core-data/src/selectors.ts
index 425a537cad7363..e110ed4e63bcd2 100644
--- a/packages/core-data/src/selectors.ts
+++ b/packages/core-data/src/selectors.ts
@@ -1182,6 +1182,34 @@ export function canUserEditEntityRecord(
return canUser( state, 'update', resource, recordId );
}
+/**
+ * Returns whether the current user can perform the given action on the entity record.
+ *
+ * Calling this may trigger an OPTIONS request to the REST API via the
+ * `hasPermission()` resolver.
+ *
+ * https://developer.wordpress.org/rest-api/reference/
+ *
+ * @param state Data state.
+ * @param action Action to check. One of: 'create', 'read', 'update', 'delete'.
+ * @param kind Entity kind.
+ * @param name Entity name
+ * @param key Optional record's key.
+ *
+ * @return Whether or not the user can perform the action,
+ * or `undefined` if the OPTIONS request is still being made.
+ */
+export function hasPermission(
+ state: State,
+ action: string,
+ kind: string,
+ name: string,
+ key?: EntityRecordKey
+): boolean | undefined {
+ const cacheKey = [ action, kind, name, key ].filter( Boolean ).join( '/' );
+ return state.userPermissions[ cacheKey ];
+}
+
/**
* Returns the latest autosaves for the post.
*