Define a capability contract

Prefer a server response tied to the current resource:

{
  "resourceId": "req_42",
  "version": 7,
  "capabilities": {
    "view": true,
    "edit": false,
    "approve": false
  },
  "reasons": {
    "edit": "This request is already closed."
  }
}

Capabilities describe the interface decision at that observed version. They are not bearer credentials and must not be trusted by the action endpoint.

The OWASP Authorization Cheat Sheet recommends validating permissions on every request and avoiding reliance on client-side access control.

Choose a control policy

For each action, decide among:

  • shown and enabled: the observed capability allows it;
  • shown and disabled: the action is relevant, but a useful reason helps;
  • hidden: the action is irrelevant or its existence should not clutter the interface;
  • loading: capability is not known yet; and
  • denied after request: the server rejected a stale or invalid assumption.

Do not display a disabled control with no explanation when the user can legitimately resolve the prerequisite.

React's conditional-rendering documentation shows the rendering mechanics. The difficult part is the business policy and backend boundary, not the JSX syntax.

Keep the component small

function ActionButton({ allowed, reason, onAction, children }) {
  if (allowed === null) {
    return <button disabled>Checking permission…</button>;
  }

  return (
    <div>
      <button disabled={!allowed} onClick={onAction}>
        {children}
      </button>
      {!allowed && reason ? <p>{reason}</p> : null}
    </div>
  );
}

This JSX is illustrative and has not been browser or assistive-technology tested. In a real table, associate the explanation programmatically with the specific control and ensure repeated messages remain understandable.

Handle stale permission correctly

Between rendering and clicking, the resource, membership, or policy may change. When the server returns a denial:

  1. do not report success;
  2. keep focus in a predictable location;
  3. explain that access or state changed;
  4. refresh canonical resource and capability data; and
  5. preserve unsaved user input when safe.

A 403 is not the same as an empty result. Do not turn denied data into “no records,” because that hides both user meaning and operational evidence.

Test five behaviors

  • An allowed control sends the resource ID and observed version.
  • A disallowed relevant action is disabled with an accessible explanation.
  • A hidden action remains blocked when its request is constructed manually.
  • A stale capability denial refreshes the screen without claiming success.
  • Removing tenant or client scope clears previously cached protected data.

The last three tests require backend and integration evidence. A component test can prove rendering policy, not authorization.

State the security boundary

The interface demonstrates capability-aware presentation, denied-state recovery, and accessible explanation. It does not demonstrate identity verification, tenant isolation, server authorization, audit completeness, or data protection. Those controls live behind the API and must be tested there.