Skip to content

Commit

Permalink
Core Data: Introduce entity-aware permission selector
Browse files Browse the repository at this point in the history
  • Loading branch information
Mamaduka committed Jul 9, 2024
1 parent 4ccb0f1 commit 3cb30c3
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 0 deletions.
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
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
84 changes: 84 additions & 0 deletions packages/core-data/src/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
28 changes: 28 additions & 0 deletions packages/core-data/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down

0 comments on commit 3cb30c3

Please sign in to comment.