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

Core Data: Introduce entity-aware permission selector #63292

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/reference-guides/data/data-core.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<https://developer.wordpress.org/rest-api/reference/>

_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.
Expand Down
3 changes: 2 additions & 1 deletion packages/block-library/src/post-title/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export default function PostTitleEdit( {
if ( isDescendentOfQueryLoop ) {
return false;
}
return select( coreStore ).canUserEditEntityRecord(
return select( coreStore ).hasPermission(
'update',
'postType',
postType,
postId
Expand Down
2 changes: 1 addition & 1 deletion packages/block-library/src/utils/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { useViewportMatch } from '@wordpress/compose';
export function useCanEditEntity( kind, name, recordId ) {
return useSelect(
( select ) =>
select( coreStore ).canUserEditEntityRecord( kind, name, recordId ),
select( coreStore ).hasPermission( 'update', kind, name, recordId ),
[ kind, name, recordId ]
);
}
Expand Down
20 changes: 20 additions & 0 deletions packages/core-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<https://developer.wordpress.org/rest-api/reference/>

_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.
Expand Down
95 changes: 85 additions & 10 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -425,16 +425,16 @@ export const canUser =
};

/**
* Checks whether the current user can perform the given action on the given
* REST resource.
* Checks whether the current user can perform the given action on the entity record.
*
* @param {string} kind Entity kind.
* @param {string} name Entity name.
* @param {string} recordId Record's id.
* @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 canUserEditEntityRecord =
( kind, name, recordId ) =>
async ( { dispatch } ) => {
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
Expand All @@ -443,8 +443,83 @@ export const canUserEditEntityRecord =
return;
}

const resource = entityConfig.__unstable_rest_base;
await dispatch( canUser( 'update', resource, recordId ) );
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.
Copy link
Member

Choose a reason for hiding this comment

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

I don't think this is right. It should indicate that the user has no permissions for the record I believe.

Copy link
Member Author

Choose a reason for hiding this comment

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

I found this comment in the original PR: https://github.com/WordPress/gutenberg/pull/12378/files#r243442316. This is similar to what we do in other resolvers - not touching the store when the request fails.

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.
*
* @param {string} kind Entity kind.
* @param {string} name Entity name.
* @param {string} recordId Record's id.
*/
export const canUserEditEntityRecord =
( kind, name, recordId ) =>
async ( { dispatch } ) => {
await dispatch( hasPermission( 'update', kind, name, recordId ) );
};

/**
Expand Down
34 changes: 28 additions & 6 deletions packages/core-data/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1173,13 +1173,35 @@ export function canUserEditEntityRecord(
name: string,
recordId: EntityRecordKey
): boolean | undefined {
const entityConfig = getEntityConfig( state, kind, name );
if ( ! entityConfig ) {
return false;
}
const resource = entityConfig.__unstable_rest_base;
return hasPermission( state, 'update', kind, name, recordId );
}

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 ];
}

/**
Expand Down
Loading
Loading