Create React Permission Utilities

Created on 1 April 2025, 3 months ago

Overview

XB UI needs to be able to check permissions and alter the UI accordingly. We need some basic utilities that will allow this to be handled consistently throughout the app.

Proposed resolution

Communicate permissions

Provide a list of permissions to the FE within drupalSettings.xb.permissions or similar.

type Permissions = string[];
interface DrupalSettings {
  xb: {
     ...
     permissions: Permissions;
  }
}

Utility functions

Create utility functions to help manage and check permissions:

  • Check if a user has an individual permission
    const hasPermission = (permission: string, userPermissions: Permissions): boolean => {
      return userPermissions.includes(permission);
    };
    
  • Check if a user has multiple permissions
    const hasPermissions = (requiredPermissions: Permissions, userPermissions: Permissions): boolean => {
      return requiredPermissions.every(permission => userPermissions.includes(permission));
    };
    
  • Check if a user has one or more of a given list of permissions
    const hasAnyPermission = (requiredPermissions: Permissions, userPermissions: Permissions): boolean => {
      return requiredPermissions.some(permission => userPermissions.includes(permission));
    };
    

React Component

A reusable React component that can be used to conditionally render UI elements based on permissions:

import React, { ReactNode } from 'react';

interface PermissionCheckProps {
  hasPermission?: string;
  hasAnyPermission?: Permissions;
  hasPermissions?: Permissions;
  denied?: ReactNode;
  children: ReactNode;
  userPermissions: Permissions;
}

const PermissionCheck: React.FC<PermissionCheckProps> = ({
  hasPermission,
  hasAnyPermission,
  hasPermissions,
  denied = <div>You don’t have permission</div>,
  children,
  userPermissions
}) => {
  const isAllowed = 
    (hasPermission && hasPermission(userPermissions)) ||
    (hasAnyPermission && hasAnyPermission(hasAnyPermission, userPermissions)) ||
    (hasPermissions && hasPermissions(hasPermissions, userPermissions));

  return <>{isAllowed ? children : denied}</>;
};

export default PermissionCheck;

Example of how it would be used:

<PermissionCheck hasPermission="canAdd" denied={(<button disabled title="You do not have permission to add">Add</button>)}>
  <button>Add</button>
</PermissionCheck>

Important Note

Backend Enforcement: Always enforce permissions on the backend as frontend checks can be bypassed.

Out of scope

Real-time Updates: Real-time updates to permissions don't need to be supported; users must reload the page to get updated permissions.

User interface changes

Design Considerations:

We need to come up with a set of common rules/UX guidelines to cover the following and when they should be used

  • UI Element Visibility: Show or remove UI elements based on permissions.
  • UI Element State: Enable or disable UI (visuals for disabled states) elements based on permissions.
  • Custom UI: Display custom UI in place of the regular UI when a permission is missing.
  • Custom dialog: Display custom dialog when a user performs an action they don't have permission to do.
✨ Feature request
Status

Active

Version

0.0

Component

Page builder

Created by

πŸ‡¬πŸ‡§United Kingdom jessebaker

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @jessebaker
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    As long as we agree on what the server will pass to the client in πŸ“Œ Pass current user's XB permissions to the XB UI Active , work on this could begin in parallel. But that probably makes little sense?

  • πŸ‡ΊπŸ‡ΈUnited States mglaman WI, USA

    πŸ“Œ Pass current user's XB permissions to the XB UI Active is finished, can the updated and agreed upon format be updated here? Are we using raw permissions from Drupal or should it be createPage, etc because that is not what was done in the other ticket.

  • πŸ‡ͺπŸ‡ΈSpain penyaskito Seville πŸ’ƒ, Spain πŸ‡ͺπŸ‡Έ, UTC+2 πŸ‡ͺπŸ‡Ί

    @mglaman That includes the config-related permissions, which are all-or-nothing so are just flags. But we might want to still postpone on πŸ“Œ Update `ApiContentControllers::list()` to expose available content entity operations in `meta` Active for the content related ones.

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    #4++

    Given that this could otherwise result in information disclosure vulnerabilities (not access bypass because the server side pieces are taken care of, at least once πŸ“Œ [PP-1] Add entity access checks to routes that deal with entities Postponed + πŸ“Œ [PP-1] Update `experience_builder.(experience_builder|api.layout.get) routes` to respect content entity update/field edit access of edited XB field Active are done), going ahead and tagging this a blocker for 🌱 Milestone 1.0.0-beta1: Start creating non-throwaway sites Active .

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    Adding #4's + #5's as related issues, to improve discoverability.

  • πŸ‡ͺπŸ‡ΈSpain penyaskito Seville πŸ’ƒ, Spain πŸ‡ͺπŸ‡Έ, UTC+2 πŸ‡ͺπŸ‡Ί
  • πŸ‡ͺπŸ‡ΈSpain penyaskito Seville πŸ’ƒ, Spain πŸ‡ͺπŸ‡Έ, UTC+2 πŸ‡ͺπŸ‡Ί

    Added the different scenarios of permissions that need to be exposed to the client UI.

  • πŸ‡ͺπŸ‡ΈSpain penyaskito Seville πŸ’ƒ, Spain πŸ‡ͺπŸ‡Έ, UTC+2 πŸ‡ͺπŸ‡Ί

    just formatting

  • πŸ‡¬πŸ‡§United Kingdom jessebaker

    In the interests of keeping things as simple as possible on the FE, I was hoping to get something like the following

    JSON:

    {
      "drupalSettings": {
        "xb": {
          [...]
          "permissions": [
            "read global regions", "update global regions", "delete global regions", "read sections", "update sections", "delete sections", ...
          ],
        }
      }
    }
    

    Because then in the UI checking if we should display a button or not is as simple as, for example:

    JSX:

    <>
        {drupalSettings.xb.permissions.includes('delete sections') && (
            <button>Delete section</button>
        )}
    </>
    
  • @jessebaker opened merge request.
  • πŸ‡ͺπŸ‡ΈSpain penyaskito Seville πŸ’ƒ, Spain πŸ‡ͺπŸ‡Έ, UTC+2 πŸ‡ͺπŸ‡Ί

    Amazing 😍
    NW per @larowlan review and I think I spotted a wrong permission check, but this looks amazing already and covers more scope than I expected.

  • πŸ‡¦πŸ‡ΊAustralia larowlan πŸ‡¦πŸ‡ΊπŸ.au GMT+10
  • πŸ‡ͺπŸ‡ΈSpain penyaskito Seville πŸ’ƒ, Spain πŸ‡ͺπŸ‡Έ, UTC+2 πŸ‡ͺπŸ‡Ί
  • πŸ‡¬πŸ‡§United Kingdom jessebaker

    Finally got to the bottom of a number of issues that were causing a lot of friction in writing the Playwright tests for this work. All resolved now and ready for another round of review.

    The summary on the MR is the most up to date place for all info on this work.

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    Did thorough manual testing. No problems found.

    Tip for fast manual testing: change

              'permissions' => [
                'globalRegions' => $this->currentUser->hasPermission(PageRegion::ADMIN_PERMISSION),
                'patterns' => $this->currentUser->hasPermission(Pattern::ADMIN_PERMISSION),
                'codeComponents' => $this->currentUser->hasPermission(JavaScriptComponent::ADMIN_PERMISSION),
                'contentTemplates' => $this->currentUser->hasPermission(ContentTemplate::ADMIN_PERMISSION),
                'publishChanges' => $this->currentUser->hasPermission(AutoSaveManager::PUBLISH_PERMISSION),
              ],
              'contentEntityCreateOperations' => $this->getContentEntityCreateOperations(),
    

    to

              'permissions' => [
                'globalRegions' => $this->currentUser->hasPermission(PageRegion::ADMIN_PERMISSION),
                'patterns' => $this->currentUser->hasPermission(Pattern::ADMIN_PERMISSION),
                'codeComponents' => FALSE,
                'contentTemplates' => $this->currentUser->hasPermission(ContentTemplate::ADMIN_PERMISSION),
                'publishChanges' => $this->currentUser->hasPermission(AutoSaveManager::PUBLISH_PERMISSION),
              ],
              'contentEntityCreateOperations' => [],
    

    etc. to test any combination of things.

    Found two bugs, both in the same area:

  • πŸ‡¬πŸ‡§United Kingdom jessebaker

    I'm not sure it's possible to actually get into those situations (yet?!) but good catch. At the moment, you can't even access XB if you don't have permission to edit XB pages so if you can access XB then you will always have at least one option in the menu.

    Anyway I've added some more robustness around the UI when people have no permissions or can delete things but don't have any other items in the menu which should keep this looking tidy in the future no matter what configuration of permissions people end up with!

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    At the moment, you can't even access XB if you don't have permission to edit XB pages so if you can access XB then you will always have at least one option in the menu.

    Is that true? What if I can create/edit articles?

    In any case, it won't be true anymore very soon, because πŸ“Œ Add permission for "Use Experience Builder" Active will definitely allow you to load XB if you cannot use Pages at all, assuming you have an editable XB field on article nodes.

    Thanks for adding that robustness β€” I won't test again, I bet you tested it thoroughly πŸ˜„πŸ‘

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    /me typed "u", return, cmd+space, click "Save" … and didn't spot that it didn't select πŸ˜…

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    🀩🀩🀩🀩 And … the XB UI actually starts to come fully alive! πŸ§Ÿβ€β™‚οΈ

Production build 0.71.5 2024