Skip to content

feat(privileges): add "All Categories" aggregate view to admin privileges page#14303

Open
palmoni5 wants to merge 3 commits into
NodeBB:developfrom
palmoni5:centralized-permissions
Open

feat(privileges): add "All Categories" aggregate view to admin privileges page#14303
palmoni5 wants to merge 3 commits into
NodeBB:developfrom
palmoni5:centralized-permissions

Conversation

@palmoni5

Copy link
Copy Markdown
Contributor

Summary

Adds an All Categories entry at the bottom of the category selector on the admin privileges page (/admin/manage/privileges). When selected it shows an aggregated view of category-scoped privileges across every category, with a third "mixed" state for privileges granted in some categories but not others. Toggling a checkbox in this view applies the change to every category at once.

Motivation

Administering privileges across many categories today means either picking categories one by one or using the per-row Copy to all categories action — which silently overwrites every existing per-category difference. There is no way to see, at a glance, which privileges are uniformly granted/denied and which are inconsistent, and no way to make a coarse change ("give group X this privilege everywhere") without losing per-category nuance elsewhere.

Behavior

For each privilege × member (group or user), the aggregated state is one of:

State Meaning UI
true granted in every category checkbox checked
false granted in no category checkbox empty
mixed granted in some categories checkbox indeterminate

Clicking a mixed checkbox once turns it on for every category (or off, depending on the resulting state) — it is always a real change, never a no-op. Rows that inherit from a parent group in the mixed state are themselves marked indeterminate, so the effective inheritance is visible rather than rendering as plain false.

Implementation notes

  • privileges.categories.listAll() (new) aggregates membership across all cids. Two new helpers in src/privileges/helpers.js (getGroupPrivilegesAll / getUserPrivilegesAll) batch the underlying getMembersOfGroups reads — one call per privilege × cid, then in-memory aggregation — instead of N round-trips per category.
  • API getPrivileges / setPrivilege handle the cid === 'all' sentinel; the latter fans give / rescind out over every cid via the existing array-cid path of helpers.giveOrRescind.
  • spawnPrivilegeStates marks a checkbox checked only when the state is literally true, so mixed renders unchecked and is then promoted to indeterminate client-side by a new Privileges.exposeMixedPrivileges(). The mixed pass runs before exposeAssumedPrivileges() so inheritance can see indeterminate parents as granted; getPrivilegesFromRow now also picks up indeterminate checkboxes for this reason.
  • categorySearch gains an appendCategories option (mirroring localCategories) that places entries after the API search results, so the All Categories pseudo-entry sits at the bottom of the dropdown rather than at the top.

Safety

  • categories.copyPrivilegesFrom now rejects a non-numeric fromCid / toCid. With the previous code, copying from an invalid source produced fromChecks = [false, false, …], which made copyPrivilegesByGroup interpret every privilege the target currently held as "in target but not in source" — i.e. every such privilege would be rescinded. The guard turns that into [[error:invalid-data]]. (This also closes the same hazard against a direct socket call, not just the UI button.)
  • The per-row Copy dropdown and the three footer copy buttons (Copy from category, Copy to children, Copy to all categories) are hidden in the All Categories view, since "copy from the aggregate" has no well-defined source; the client also guards the three copy functions defensively.

Files changed

Area File
Backend src/privileges/helpers.js, src/privileges/categories.js, src/api/categories.js, src/controllers/admin/privileges.js, src/categories/create.js
Template src/views/admin/partials/privileges/category.tpl
Client public/src/admin/manage/privileges.js, public/src/modules/categorySearch.js, public/src/modules/helpers.common.js
i18n public/language/en-GB/admin/manage/privileges.json (all-categories, alert.copy-disabled-all)

Out of scope / non-goals

  • Global (cid 0) and Admin privileges are unchanged — All Categories aggregates only category-scoped (cid > 0) privileges, mirroring the scope of the existing per-category view.
  • No new privilege types are introduced; this is a pure presentation/edit-surface change.

Manual verification

  • Backend listAll() returns groups with state ∈ {true, false, 'mixed'} once at least one category has a differing privilege.
  • Toggling a checkbox in the All Categories view issues a single PUT/DELETE /api/v3/categories/all/privileges/:privilege call which fans out across cids.
  • Attempting the legacy copy paths with cid === 'all' is blocked by both the UI (buttons hidden) and the server-side guard in copyPrivilegesFrom.

Add an "All Categories" entry at the bottom of the admin privileges
category selector that shows an aggregated view of category-scoped
privileges across every category. Checkboxes display the combined
state per privilege/member:

  - checked        granted in every category
  - empty          granted in no category
  - indeterminate  granted in some categories only ("mixed")

Toggling a checkbox in this view applies the change to every
category at once. Rows that inherit from a parent group in the mixed
state are also marked indeterminate so the effective inheritance is
visible.

Implementation
- privileges.categories.listAll() aggregates membership across all
  cids; new helpers (getGroupPrivilegesAll / getUserPrivilegesAll)
  batch the underlying set reads in privileges/helpers.js.
- API getPrivileges/setPrivilege handle the cid="all" sentinel and
  fan the give/rescind out over every cid.
- spawnPrivilegeStates only marks a checkbox 'checked' when the
  state is literally true, leaving "mixed" rendered as
  indeterminate via exposeMixedPrivileges().
- categorySearch supports an appendCategories option so the
  "All Categories" entry sits after the real category list.

Safety
- copyPrivilegesFrom now rejects a non-numeric source/target cid,
  preventing the bulk-revoke that would otherwise occur when copying
  from the "all" pseudo-cid (fromChecks would be all-false).
- The per-row copy dropdown and the footer copy buttons are hidden
  in the "All Categories" view; the client also guards the copy
  actions defensively.
@nodebb-misty

Copy link
Copy Markdown
Contributor

💡 Friendly Note

This pull request was made against the develop branch, which is reserved for commits destined for a minor or major release.If your commits simply fixes a bug, please rebase this PR against the master instead.

  • patch releases — bug fixes only
  • minor releases — new features, enhancements, and bug fixes
  • major releases — breaking changes, including all of the above

Thanks!

@palmoni5

palmoni5 commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@barisusakli I would be happy to receive a response from you.

@barisusakli

Copy link
Copy Markdown
Member

I will have to review this but I like the idea of having a single page to apply the change to all categories.

@barisusakli barisusakli self-requested a review June 3, 2026 13:49
Comment thread public/src/modules/categorySearch.js Outdated
Comment thread src/views/admin/partials/privileges/category.tpl Outdated
Comment thread src/views/admin/partials/privileges/category.tpl Outdated
@barisusakli barisusakli added this to the 4.13.0 milestone Jun 3, 2026
palmoni5 added 2 commits June 3, 2026 19:20
Address review feedback on NodeBB#14303: benchpress supports != so the
empty if-branch + else pattern can be collapsed into a single if.
No behavior change.
Per review on NodeBB#14303: placing the aggregate entry at the bottom hides
it when the forum has many categories. Move it back to the head of
the local-prepended list and drop the now-unused appendCategories
plumbing in categorySearch.js (no diff vs upstream there now).
@palmoni5

Copy link
Copy Markdown
Contributor Author

All are corrected.

@barisusakli barisusakli modified the milestones: 4.13.0, 4.14.0 Jun 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants