Multi-role table permissions, server-side enforced, shortcode-overridable
Restrict any table to specific WordPress user roles. Multi-select, server-enforced, with shortcode overrides for special cases. The pattern that unlocked client portals.
- Restrict table visibility to one or more WordPress user roles
- Server-side enforcement, unauthorized users get an access-denied message, never the data
- Multi-select admin UI, pick any combination of roles per table
- Shortcode override (`allowed_roles="X,Y"`) for one-off page configurations
- Backwards compatible with the older single-role filter
Before 2.0.2, role-based access in Gravity Tables was a binary: either anyone could see the table, or you wrote PHP. Most teams ended up with a current_user_can( 'X' ) check in a custom template wrapper, which worked, but didn’t survive a theme switch and definitely didn’t audit cleanly.
What 2.0.2 ships#
A new field in the table builder:
Allowed roles, pick one or more WordPress roles. Users without any of these roles see “You don’t have permission to view this table”.
Works exactly like you’d hope:
- Multi-select dropdown listing all registered WP roles (including custom ones)
- Saved as a JSON array on the table settings record
- Checked server-side before the table query runs, unauthorized requests never touch the entries table
- Translatable error message (
gravity_tables_no_permissionfilter for custom messages)
The shortcode override#
Sometimes a single page needs a different role set than the table’s default. For that, the shortcode now accepts allowed_roles:
[gravity_table id="42" allowed_roles="customer,subscriber"]
This replaces the table-level setting for that specific shortcode invocation. Useful when:
- Same underlying table is shown on a public page (
allowed_roles="*") and a staff-only page (allowed_roles="manager") - A landing page needs to gate by a custom role that doesn’t apply to the table’s default audience
- A temporary campaign page wants tighter restrictions than the long-term table view
The special token * allows any logged-in user. Empty string allows guests.
How it interacts with filter_by_user#
Two layers, both must pass:
allowed_roles, does this user belong to a role that can see the table at all?filter_by_user, once they can see the table, narrow to their own entries
Layer 1 returns 403 if it fails. Layer 2 returns an empty result set if the user has no entries. The two are independent and compose cleanly.
Server-side enforcement, by design#
A common pattern in less-careful plugins:
// BAD: client-side enforcement
if (!current_user_can('manage_options')) {
echo '<style>.admin-cell { display: none; }</style>';
}
That hides the cells visually. The data is still in the HTML. View source, screenshot, scrape, all trivial.
2.0.2 uses the database query as the gate:
// GOOD: server-side enforcement
if (!user_in_allowed_roles($table->allowed_roles)) {
return $this->render_access_denied();
}
$entries = $this->repository->find_by_table($table->id);
If the role check fails, the entry query never runs. The database doesn’t yield rows. There’s nothing to leak.
Backwards compatibility#
Tables created before 2.0.2 had a single role_filter field. Activation runs a one-time migration:
role_filter = ''→allowed_roles = [](anyone can see, same as before)role_filter = 'subscriber'→allowed_roles = ['subscriber']role_filter = 'subscriber,customer'(older comma-string) →allowed_roles = ['subscriber', 'customer']
The migration is idempotent and runs in < 100ms even on sites with thousands of tables.
What this enabled#
Multi-role permissions are the foundation under almost every shipped use case:
- Customer portals, gated to
customer+subscriber - Healthcare intake, split between
clinician(full chart) andnurse(read-only) - Team collaboration, different teams gate to their own role
- HR onboarding, HR sees the full pipeline, managers see their team
Without per-role gating, all of those would have needed custom PHP. With it, they’re a checkbox.