Description

WordPress keeps every role in one autoloaded option, with each role’s capabilities copied out in full, because core has no inheritance. On a membership or commerce site with forty roles that option is read, unserialized and held in memory by every PHP worker on every request — and those are exactly the sites whose traffic is mostly logged in, so page caching never gets a look in.

LeanRoles does two things about it.

It measures. The auditor is strictly read-only. It takes the size of your role option with LENGTH() in the database rather than re-serializing it in PHP, times a real unserialize() of it on your machine after a warm-up pass, measures the resident footprint with the result kept alive, and works out what that costs in concurrent workers. It identifies roles that grant no effective permission, roles nobody holds, roles with identical capability sets, and capabilities it cannot account for — while being explicit that “unrecognised” is not “orphaned”, because custom code checks capabilities no scanner can see.

It offers the missing primitive. There is no way in WordPress to say “this user is a wholesale customer” without granting them permissions. There are only roles. That is why membership plugins keep inventing them: they have no alternative. A LeanRoles tag is that alternative. It appears in $user->roles, answers current_user_can(), and can be filtered on in WP_User_Query — and it is never written to the autoloaded option. Third-party code cannot tell the difference.

What it does

  • The auditor, with real measurements and wp leanroles audit --format=json
  • User tags: create, assign individually or in bulk, filter, a users-list column, CSV import and export
  • The tag engine as a standalone library, libraries/user-tags/, that any plugin can bundle so tags cost them nothing to adopt
  • WP-CLI: create a tag, assign it in bulk by role, edit or delete a role with reassignment, move a role configuration between sites, take and restore role-option backups

Licensing, and the one external service

LeanRoles uses Freemius for licensing, payment and automatic updates on its paid plan. Freemius is a third-party service, and this is the only thing in the plugin that talks to anything outside your site.

Nothing is sent anywhere unless you say so. The plugin asks once, on activation, and skipping is a real option: skip it and the plugin works exactly as it does otherwise. Nothing about the auditor or user tags depends on it.

If you do opt in, what is shared is:

  • your WordPress user’s name and email address;
  • the site’s homepage URL and title, its language, and the WordPress and PHP versions;
  • which version of LeanRoles is running, its SDK version, and whether it is active or has been uninstalled.

Two further items are optional and stay off unless you tick them: the list of other plugins and themes installed on the site, and the newsletter.

Activating a licence key sends the key and the site URL, so the licence can be checked and updates delivered to you.

Freemius’ terms and privacy policy cover what they do with it.

What the paid plans cost, and what they add

What it does not do

It does not convert roles into tags for you. What it gives you are three independent primitives — create a tag, assign it in bulk, delete a role with reassignment — which compose into a conversion done by hand, by someone who has read the audit and accepts the risk.

Before deleting a role it tells you how many capabilities that role grants and how many users hold it. It does not tell you which of those users will actually notice the difference: answering that means computing effective capabilities per user, before and after, and the plugin does not do it. Take a backup and check for yourself.

Development

The distributed plugin has no dependencies and no build step: what is in src/ and assets/ is what runs. Composer is used only for the test suite, and .distignore keeps all of it out of the release.

composer install
./tests/bin/start-db.sh      # throwaway MariaDB on 127.0.0.1:3307
./tests/bin/install.sh       # WordPress + the test library
composer test                # single site
composer test:multisite      # network
composer test:matrix         # WordPress 5.9, 6.5 and latest
composer lint                # standards + PHP 7.4 compatibility

Every test runs against a real WordPress install and a real database, because the risky parts of this plugin are all seams it does not own — the short-circuit contract of the metadata filters, what WP_User::set_role() does on its way past, how WP_User_Query builds a role clause. See tests/README.md.

Screenshots

Installation

  1. Upload the plugin to wp-content/plugins/leanroles and activate it.
  2. Open LeanRoles Audit. Nothing on that screen writes anything.

FAQ

Is the auditor safe to run on a production site?

Yes, and that is the point of it. It reads. The only things it writes are a twelve-hour cache of count_users() and, on activation, a restore point of your role option.

What happens to my tags if I deactivate the plugin?

They stop being injected. Because they grant no capabilities, nothing about what your users can do changes. The assignments stay in the database and come back when you reactivate.

Can my plugin use user tags without depending on LeanRoles?

Yes, and that is the point. The tag engine is a self-contained library that ships inside this plugin and can be copied into yours: one require_once and you have user_tags_add(), user_tags_get() and the rest. If several plugins bundle it, the newest copy wins and they all share one set of tags. Removing LeanRoles does not remove them. See libraries/user-tags/readme.md.

Will `current_user_can(‘my_tag’)` work?

Yes. So will in_array( 'my_tag', $user->roles ), get_users( array( 'role' => 'my_tag' ) ) and WP_Roles::is_role().

Does it work on multisite?

The auditor understands it: role options are per site and it reads the right one. Tags are per site too, since term relationships live in each site’s own tables. Network-wide operations are not in this version.

Does an object cache solve the underlying problem?

It helps with query time and nothing else. Memory, unserialization and — with Redis — bandwidth all get worse, not better, because alloptions lives under a single key and crosses the wire whole on every request. The audit detects your drop-in and tones its findings down if that drop-in already compresses or splits the blob.

Reviews

There are no reviews for this plugin.

Contributors & Developers

“LeanRoles” is open source software. The following people have contributed to this plugin.

Contributors

Translate “LeanRoles” into your language.

Interested in development?

Browse the code, check out the SVN repository, or subscribe to the development log by RSS.

Changelog

0.6.1

The report screen named the filter that puts an agency’s own name on a report and left it at that, which makes a feature only its author can use. It now carries the whole recipe — the call to paste into a mu-plugin, and a table of where each of the three keys comes out. Writing that table down turned up a key that was carried everywhere and rendered nowhere; it is rendered now, and the one output that cannot show an image says so instead of pretending.

This affects the paid plans only. Nothing in this build changes.

0.6.0

Five screens in wp-admin for what the paid plans do: converting a role to a tag, giving a role a parent, bulk tagging, the report with its thirty-day drift, and drift alerts. All of it already existed and all of it was reachable only from WP-CLI — a reasonable interface for somebody who lives in a terminal, and no interface at all for everybody else.

The screens decide nothing themselves. They call what the commands call, so a screen and a terminal cannot disagree about the same site. Two things they do differently on purpose: the code scan that reads every PHP file under wp-content runs when it is asked for rather than on page load, and a bulk run advances one batch per click rather than pretending a web request can walk fifty thousand users.

This affects the paid plans only. Nothing in this build changes.

0.5.6

The Author URI header pointed at a wordpress.org profile page. It points at davefx.com, which is where the other plugins under this name already point.

0.5.5

A link, in the description, to the page that says what the paid plans cost. 0.5.4 pointed the Plugin URI header there, which was the right value for that header and does not put a link on this page — the directory does not render it. A line in the description does.

0.5.4

The Plugin URI header — what this directory shows as the plugin’s home page — pointed at a GitHub repository. That repository is generated: it holds the built plugin and is replaced wholesale on every release, so it was never a home page and is a stranger place to send somebody now than it was. It points at the plugin’s own page instead.

0.5.3

The other half of 0.5.2. Unwrapping the paragraphs left the bulleted lists wrapped, because their continuation lines are indented and the pass that fixed the paragraphs mistook that indentation for a code block. Thirteen breaks survived, all inside lists. There is now a test that reads this file and fails if any line continues the one above it, so the next one cannot reach the directory.

0.5.2

Presentation only, and none of it in the plugin. The readme’s paragraphs were hard-wrapped at eighty columns, and wordpress.org keeps every line ending as a line break, so the listing showed text broken mid-sentence wherever a paragraph had been wrapped — and read correctly wherever one happened to have been written as a single long line. They are all single lines now. The listing also has a banner, which it did not before.

0.5.1

wp leanroles bulk could not stop. It ran passes until one reported itself finished, and if a pass ever stopped making progress there was nothing to end the loop — a command that never returns, or a cron worker pinned until the host kills it. It now abandons the run when a pass moves nobody, says which user it stopped at, and leaves the run resumable rather than undoing what it had done.

This affects the paid plans only. Nothing in this build changes.

0.5.0

wp leanroles tag export and `wp leanroles role export` no longer take `--file`. Both print to standard output, so redirect them where you want them: `wp leanroles role export > roles.json`. The plugin now writes to the filesystem nowhere at all.

Every string the plugin ships, including those in the bundled User Tags library, now uses one text domain, so all of them can be translated through translate.wordpress.org. Compiled translations are no longer bundled — the directory serves those, and community translations reach you the moment they are approved rather than the next time this plugin is released.

0.4.0

Two primitives the free tier was missing.

wp leanroles role update changes a role's display name and what it grants. Command line only, and that is the product decision rather than an omission: a screen of capability checkboxes would put this in the category of role editors, and this is an auditor. It keeps three things apart that are easy to conflate — granting a capability, denying one, and dropping it altogether — because a denied capability still occupies a row in the option, which is the thing being measured.

wp leanroles role export and `import` move a configuration between sites. Not the same as a restore point: a backup comes back to the site it came from, an export leaves, so it records where it came from and when. Protected roles are never touched in either direction, so a file that simply omits `administrator` cannot be a way to delete it.

Spanish is complete again. The catalogue had fallen behind the code by about a third.

0.3.0

Licensing, through Freemius. The SDK ships in libraries/freemius/ and handles the paid plan, licence activation and automatic updates.

It is the only part of the plugin that contacts anything outside your site, and it asks first. The opt-in on activation is skippable, and skipping costs you nothing: the auditor and user tags neither know nor care. What is shared if you do opt in, and what stays optional, is set out under Licensing, and the one external service above.

The SDK is required explicitly rather than autoloaded, for the same reason as the User Tags library: Freemius also arbitrates between every copy installed on a site and boots the newest, and an autoloader binding a class to a copy other than the one that won is the failure that pattern exists to avoid.

The plugin’s test suite now loads it the way WordPress does, from inside wp-content/plugins. Loaded from anywhere else the SDK cannot find the plugin’s main file, resolves it to the plugins directory, and warns on every read of the plugin headers.

0.2.2

Housekeeping in what ships, with no functional change.

The bundled library’s readme.txt no longer travels in the archive. It is written for the wordpress.org directory, and a second directory readme inside one plugin is something a reviewer has to stop and work out. Its readme.md, which documents bundling the library, stays — whoever goes looking in libraries/user-tags/ wants that one.

The library is 1.3.4, which is called User Tags again. The name had been changed to claim a wordpress.org permalink for it; it is not being listed there separately, so the name was buying nothing.

0.2.1

The Users Tags link works. The archive published as 0.2.0 carried an earlier build of the bundled library, in which renaming the text domain had swallowed the admin page slug — the two were the same string. The menu item pointed at a page that did not exist, and clicking it said you were not allowed to access it. Nothing was wrong with your permissions.

The library is now 1.3.3, which also means: activated on its own it registers its own screens, so a site can install User Tags without LeanRoles; and the whole plugin clears the wordpress.org Plugin Check. That last one found a handful of things worth fixing here too — a stale Tested up to, a hidden file inside the archive, three unescaped integers in the audit’s worker-model inputs, and is_writable() where core’s wp_is_writable() belongs.

0.2.0

The tag screens moved to the bundled library. They now sit under Users Tags, which is where an administrator looks for something that belongs to users, rather than under the LeanRoles menu. Behaviour is the same; the code is now shared.

The library is User Tags, bundled in libraries/user-tags/ and free for any plugin to adopt. LeanRoles asks it for the screens the same way any other consumer would:

add_filter( 'user_tags_enable_admin', '__return_true' );

Spanish translation. languages/ carries the .pot and es_ES, and the plugin loads its own text domain rather than waiting for one to be loaded for it, so the strings arrive whichever way the plugin was installed.

The LeanRoles menu now holds the audit and nothing else.

0.1.0

First release.

The auditor. Strictly read-only, so it can be left on a client site without asking anyone first. It measures rather than estimates:

  • Size of the role option taken with LENGTH() in the database, not by re-serializing in PHP — the two produce different strings, and the difference is exactly the sort of small dishonesty that gets a performance plugin disbelieved.
  • A real unserialize() of your own option, timed on your own machine after a warm-up pass.
  • Resident footprint measured with the result kept alive, and the ratio of in-memory to on-disk bytes.
  • What that costs in concurrent PHP workers, and in object-cache bandwidth, from inputs you can adjust.
  • Roles that grant no effective permission, roles nobody holds, roles with identical capability sets, subset relationships, deprecated level_N entries, and a conservative lower bound on what inheritance could remove.
  • Capabilities it cannot account for — reported as unrecognised, never as orphaned, because custom code checks capabilities no scanner can see.
  • Detection of the object-cache.php drop-in, with the findings toned down when the drop-in already compresses or shards the blob.

User tags. A label that behaves like a role to the whole of WordPress — it appears in $user->roles, answers current_user_can(), and WP_User_Query finds it — while granting no capability and never being written to the autoloaded role option. Create and edit tags, assign them individually or in bulk from the users list, filter by them, CSV import and export.

The tag engine ships as a standalone library, libraries/user-tags/, that any plugin can bundle. See https://github.com/davefx/user-tags

WP-CLI.

  • wp leanroles audit — including --format=json for aggregating across sites
  • wp leanroles tag create|delete|list|assign|remove|users|export|import
  • wp leanroles role delete --reassign=<slug> with a --dry-run
  • wp leanroles backup create|list|restore

Bulk assignment runs in batches, releases the users it touched, and reports the last id it reached so an interrupted run can be resumed rather than restarted.

Requirements. WordPress 5.9+, PHP 7.4+. Multisite aware. No dependencies and no build step.