keepcalculator

06 Numbers and Powers

Map Backend Permission Strings to UI Actions

Card No. 06·64873 · Updated · Standard formula, shown below

Mapping backend permission strings to UI actions means defining a clear, consistent way for your frontend to know which buttons, menus, or routes a user can see or use based on the permissions your backend returns. This article explains the core concepts, common patterns, and practical steps to implement this mapping correctly in your application.

Background: Why Permission Strings Exist

Backend permission strings (like order:create or report:view) are a standard way for servers to communicate what a user is allowed to do. They are usually returned as an array of strings after login or fetched from an endpoint like /me/permissions. These strings are often derived from roles (e.g., admin, editor) or directly assigned to users. The backend enforces these permissions on every API call, but the frontend also needs to know them to hide or disable actions the user cannot perform. Without this mapping, users see buttons that fail with 403 errors, which is poor UX. The mapping is essentially a translation layer between the backend’s string-based permission model and the frontend’s UI components.

How Permission Strings Are Structured

Most backends use a hierarchical or namespaced format for permission strings. The most common convention is resource:action or resource.action. For example:

  • invoice:read – view an invoice
  • invoice:create – create a new invoice
  • invoice:update – edit an existing invoice
  • invoice:delete – remove an invoice

Some systems use wildcards like invoice:* to mean all actions on a resource, or *:* for full access. Understanding this structure is crucial because your mapping logic will likely need to parse these strings to make decisions. For example, if a user has invoice:*, the UI should show all invoice-related buttons. If they only have invoice:read, only the view button appears.

Mapping Strategies: Direct vs. Role-Based

There are two main approaches to mapping permissions to UI actions:

  • Direct mapping: Each UI action (button, menu item, route) is associated with a specific permission string. The frontend checks if the user’s permission array contains that string (or a wildcard match) before rendering the element.
  • Role-based mapping: The frontend maps roles (e.g., admin) to a set of UI actions. This is simpler but less flexible because roles can change without the frontend knowing. It also duplicates logic that should live on the backend.

Direct mapping is generally recommended because it keeps the frontend in sync with backend authorization rules. You define a mapping object or function that translates a permission string into a boolean for a specific UI element. For instance:

const can = (permission) => userPermissions.includes(permission) || userPermissions.includes('*:*');

// Usage: <button v-if="can('invoice:create')">New Invoice</button>

This approach scales well and avoids hardcoding roles in the frontend.

Implementing the Mapping in Your Frontend

Here is a practical step-by-step guide to implementing permission mapping in a typical JavaScript frontend (React, Vue, Angular, etc.):

  1. Fetch permissions: After login, retrieve the user’s permission list from the backend and store it in a global state (e.g., Redux, Vuex, or a context).
  2. Create a permission helper: Write a function that takes a permission string and returns true if the user has it. Include wildcard handling (e.g., resource:* matches any action on that resource).
  3. Use the helper in components: For each UI action, wrap it with a conditional check. For example, in React: {can('invoice:delete') && <DeleteButton />}.
  4. For routes: In your router configuration, add a meta field that requires a specific permission. In the navigation guard, check the permission and redirect if not allowed.
  5. For menus: Filter the menu items based on permissions before rendering.

Remember to also handle the case where the permission list is empty or not yet loaded (show a loading state).

Common Problems and Solutions

Several issues arise when mapping permissions to UI actions:

  • Wildcard mismatches: If your backend uses invoice:* but your frontend checks for invoice:read, it won’t match. Solution: implement a wildcard matching function that splits the string and compares segments.
  • Permission string changes: Backend teams may rename permissions. To avoid breaking the UI, keep a single source of truth (e.g., a constants file) and use it in both the helper and the backend documentation.
  • Over-permissioning: Showing a button that the backend rejects is bad, but hiding a button that the user actually has permission to use is also bad. Ensure your permission list is always up to date (e.g., refetch on route change if needed).
  • Performance: Checking permissions on every render can be costly. Use memoization or compute a permissions object once and pass it down.

Another common misconception is that hiding UI elements is sufficient for security. It is not – always enforce permissions on the backend. The frontend mapping is only for UX.

Choosing the Right Approach for Your App

Deciding between direct mapping, role-based, or a hybrid depends on your app’s complexity:

  • Small apps with few roles: Role-based mapping is faster to implement, but be cautious when roles change.
  • Large apps with many resources and actions: Direct mapping with wildcards is more maintainable.
  • Apps with dynamic permissions: If permissions are assigned per user (not just roles), direct mapping is the only viable option.

Also consider using a library like casl (for JavaScript) that provides a more sophisticated ability system. However, for simple use cases, a custom helper is often enough.

Alternatives to String Mapping

Some systems use numeric permission bits or boolean flags instead of strings. For example, a bitmask where bit 0 means read, bit 1 means write, etc. This is more compact but less readable. Another alternative is to have the backend return a list of allowed actions directly (e.g., ['read', 'create'] for each resource). This reduces parsing but can be verbose. Strings are the most common and flexible, so they are worth sticking with unless you have a specific need.

Frequently Asked Questions

How do I handle wildcard permissions like *:*?

Implement a function that splits the permission string into parts and checks if each part matches the required permission. For example, if the user has invoice:*, it should match invoice:read. A simple approach is to convert both the user permission and the required permission into arrays and check if the user’s array is a prefix of the required array, or if the user’s array is exactly ['*'].

What if the backend returns roles instead of permission strings?

You have two options: either ask the backend to also return permissions (derived from roles) or map roles to permissions on the frontend. The latter is less secure and can become outdated. It’s better to have the backend resolve roles to permissions and send the final list.

Should I hide or disable UI actions when the user lacks permission?

It depends on the action. For destructive actions (delete), hiding is often better to avoid confusion. For actions that might be available under certain conditions (e.g., edit only when status is draft), disabling with a tooltip is more informative. A common pattern is to disable buttons and show a tooltip explaining why.

How do I test permission mapping?

Write unit tests for your permission helper with different permission arrays (including wildcards and empty arrays). Also do integration tests by simulating different user roles and checking that the UI renders the correct elements. Use mock permission data in your frontend tests.

Can I use CSS to hide elements based on permissions?

No, CSS is not a security boundary and is easily bypassed. You must conditionally render elements in your JavaScript framework. CSS hiding is only acceptable for purely cosmetic purposes, never for access control.

What is the best way to store permissions on the frontend?

Store them in a global state (Redux, Vuex, or React Context) so that any component can access them. Avoid storing in local storage for security reasons, but if you do, always re-fetch on page load. In-memory state is sufficient for most single-page applications.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →