Skip to content
Gravity Tables
minor v2.0.2 · · 3 min read

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_permission filter 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:

  1. allowed_roles, does this user belong to a role that can see the table at all?
  2. 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:

Without per-role gating, all of those would have needed custom PHP. With it, they’re a checkbox.

#permissions#roles#security