Permission gates
Present managed RBAC decisions with PermissionGate, RequireAuth, and the verified permission claim.
Present managed RBAC decisions with PermissionGate, RequireAuth, and the verified permission claim.
Permission gates answer one question: does the signed-in member's current role grant a permission? They do not answer whether the plan grants a route or whether a usage limit currently admits the request.
Permission keys use the same grammar as gateway enforcement: exact
subject:verb, subject wildcard subject:*, or global *. There is no implicit
verb widening.
import {
AccessDenied,
PermissionGate,
} from "@farthershore/farthershore-js/components";
<PermissionGate
permission="reports:write"
mode="denied"
fallback={<AccessDenied requiredPermission="reports:write" />}
>
<ExportReportButton />
</PermissionGate>;
While auth and authorization resolve, the component renders an aria-busy placeholder instead of flashing protected content.
PermissionGate supports four deny modes (the contracts gate-mode
vocabulary):
hide renders nothing;denied renders the supplied fallback node, or a default AccessDenied
panel naming the missing permission;disable wraps children in an inert disabled fieldset;readOnly renders children while usePermissionReadOnly() returns true.For a bare permission gate the default is hide — hiding an affordance the
member cannot use. For a component gate (below) the default comes from the
resolved component policy: denied for most components, hide for the
security-sensitive set (audit_log, api_keys_panel, team_panel), unless
the builder registration or a subscriber-org override configured one.
Components that support read-only composition can consume the context:
import {
PermissionGate,
usePermissionReadOnly,
} from "@farthershore/farthershore-js/components";
function ReportEditor() {
const readOnly = usePermissionReadOnly();
return <textarea readOnly={readOnly} />;
}
<PermissionGate permission="reports:write" mode="readOnly">
<ReportEditor />
</PermissionGate>;
Pass component instead of permission and the gate resolves the permission
AND the deny render through the fail-closed component-policy resolver —
builder registration, then the subscriber organization's override, then the
managed defaults:
<PermissionGate component="audit_log">
<AuditLogTable />
</PermissionGate>
The same resolution backs withGate (wrap once, gate everywhere the component
mounts) and the useComponentGate hook:
import {
useComponentGate,
withGate,
} from "@farthershore/farthershore-js/components";
const GatedReports = withGate(ReportsPanel, "custom:reports");
function ReportsNavLink() {
const gate = useComponentGate("custom:reports");
if (gate.status !== "granted") return null;
return <a href="/reports">Reports</a>;
}
Register your own component ids and let subscriber orgs configure them — see Custom components.
import { usePermissionGate } from "@farthershore/farthershore-js/components";
function ExportButton() {
const permission = usePermissionGate("reports:export");
if (permission.status !== "granted") return null;
return <button>Export</button>;
}
Or use the current auth surface when several checks share one component:
const auth = useFsAuth();
if (auth.authzLoaded && auth.hasPermission("team:invite")) {
// render invite action
}
The claim is resolved by the server. Do not decode a token in the browser or replace it with a locally cached list.
RequireAuth can also require a permission. A signed-out visitor follows the
auth redirect path; a signed-in but under-permissioned member sees the fallback
without a redirect:
<RequireAuth
requirePermission="audit_log:read"
fallback={<AccessDenied requiredPermission="audit_log:read" />}
onRedirect={(target) => navigate(target)}
>
<AuditLogPage />
</RequireAuth>
AccessDenied can receive an onRequestAccess callback when your product wants
to create an access request. Keep that workflow distinct from an upgrade prompt:
roles are controlled by the subscriber organization; plans are commercial
entitlements.
Route-level permission requirements are enforced before forwarding. For a record- or field-level rule inside a handler, use the verified backend context:
import { requirePermission } from "@farthershore/backend";
fs.handler(async (ctx, req, res) => {
requirePermission(ctx, "reports:export");
// still scope the record query to ctx.principal.org.id
});
Every frontend gate is UX. A user can call an endpoint without rendering your component. The compiled gateway policy, verified backend context, and tenant-scoped database query are the authorization boundary.