Skip to content

Sync 3.5 to 3.6 - #2579

Merged
Seiger merged 6 commits into
evolution-cms:3.6.xfrom
elcreator:sync-3.5-to-3.6
Nov 10, 2026
Merged

Seiger merged 6 commits into
evolution-cms:3.6.xfrom
elcreator:sync-3.5-to-3.6

Conversation

@elcreator

@elcreator elcreator commented Nov 9, 2026 •

Copy link
Copy Markdown

Follow-up to #2577, which was merged before the review of #2578 was addressed.

Brings in #2578 (against 3.5.x, still open). Role reach now compares the grants that authorize at runtime, on both sides:

  • RoleGuard::heldBy(): the actor's own hasPermission() for every defined key and every key any role is granted, custom ones included;
  • RoleGuard::reachOf(): the target's stored grants, always-on ones included, less only the obsolete role_actionok / action_ok the earlier seeders left;
  • a role save stores only grants the actor holds, and assignment goes through RoleGuard::mayAssignRole() (save_role plus reach);
  • the user form's role id is validated once (RoleGuard::postedRoleId(): "0" or the decimal id of an existing role) before anything is saved, and that same int is checked and stored. Before, the posted string reached setRole(), and MySQL/MariaDB in the installer's non-strict sql_mode could coerce it into a role that was never checked.

This replaces the earlier version merged with #2577, which compared only defined permissions. That let a role carrying a plugin key with no permissions row be saved, assigned or deleted by a manager who does not hold the key.

Role save, delete and assignment use the same helpers. 3.6 has no other role reach check.

Verification: merges into 3.6.x without conflicts. Full suite in evo-ci-php:8.4: 2739 passed, 12 skipped; UserRoleAssignmentTest also on MySQL 8.4. PHPStan: 0 errors.

🤖 Generated with Claude Code

elcreator and others added 2 commits November 9, 2026 19:06
At login the session holds every stored grant of the role, defined by a
permissions row or not, so a plugin's own key authorizes as much as a core
one. adb4e31 compared only grants of defined permissions on both sides: a
role carrying such a key could be saved, assigned or deleted by a role
manager who does not hold it.

Both sides now follow the runtime grants. RoleGuard::heldBy() asks the
actor's hasPermission() for every defined key and every key any role is
granted; RoleGuard::reachOf() takes the target's stored grants without the
obsolete keys the earlier default seeders left (RoleGuard::OBSOLETE:
role_actionok, action_ok, which nothing checks) and without the always-on
permissions, which a role save stores on every role whoever saves it.
Role save, delete and assignment use the same two helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto 3.6.x

Both sides of the role reach check follow the grants that authorize at
runtime, custom keys included; only the obsolete role_actionok/action_ok
and the always-on permissions are left out (review of evolution-cms#2578).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MiddleSokilAI

Copy link
Copy Markdown
Collaborator

I would hold the merge at 39f789d until the remaining role-authorization concern is resolved. This sync includes the same disabled/always-on permission exception discussed in the latest #2578 review. Please keep the reach comparison consistent with effective runtime grants for each operation, and cover varied delegated save/assign/delete permissions and existing roles with controller-level tests.

There is also a separate consistency issue in the assignment controller: the authorization check uses an integer-cast role ID, while the original input is passed on for persistence. That could make the checked and stored role differ depending on input handling and database coercion. Please strictly validate the role ID once and use that same canonical, checked value throughout authorization and persistence. Add a regression test proving that the role written is exactly the role whose permissions were checked, including rejection of invalid or ambiguous input.

These are static review observations. I have not reproduced either case at runtime, and the database-coercion consequences of the role-ID mismatch remain unverified.

SokilAI

elcreator and others added 2 commits November 9, 2026 19:41
… rule

044ecd3 left always-on permissions out of a target role's reach, because a
role save stores them on every role. Only a manager holding save_role can
save, though: one allowed to delete a role, or holding a narrower set, could
act on a role whose always-on grants it lacks, and those grants authorize at
runtime like any other.

reachOf() now leaves out only the obsolete keys. A non-administrator stores
on a saved role only grants it holds, always-on ones included. The decision
EditOrNewUser makes before it changes a user's role, save_role plus the
reach check, moves to RoleGuard::mayAssignRole().

The tests run the real role save and delete and mayAssignRole() for
save_role / delete_role combinations, fresh and upgraded installs, custom
permissions with and without a permissions row, and an actor whose role
lacks an always-on grant the target holds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3.6.x

Always-on grants count in role reach, a role save stores only grants the
actor holds, and assignment goes through RoleGuard::mayAssignRole()
(second review of evolution-cms#2578).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
elcreator and others added 2 commits November 9, 2026 23:55
EditOrNewUser checked the reach of (int) $_POST['role'] but passed the
posted string to setRole(). MySQL and MariaDB in the sql_mode the
installer configures coerce it on write ('0.5' becomes 1), so the stored
role could be one the check never saw. RoleGuard::postedRoleId() now
accepts only "0" or the decimal id of an existing role, validated once
before anything is saved, and that same int goes to the TV lookup, the
reach check and setRole().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@elcreator

elcreator commented Nov 9, 2026 •

Copy link
Copy Markdown
Author

Thanks, both points are fixed on #2578 and merged here.

  • Always-on exception: removed in fec047f (merged as 3e7e3b1). Role reach now leaves out only the obsolete role_actionok / action_ok. Each operation needs its own permission plus the reach check, and controller-level tests cover the save_role / delete_role combinations on fresh and upgraded installs. Details: Compare role reach by the grants that authorize at runtime #2578 (comment)
  • Role id consistency: confirmed at runtime and fixed in 75dc25b (merged as b0e8949). In the installer's non-strict sql_mode, MySQL/MariaDB coerce the posted string setRole() received, so the stored role could differ from the checked one. The role id is now validated once (RoleGuard::postedRoleId(): "0" or the decimal id of an existing role), before anything is saved, and that same int is checked and stored. UserRoleAssignmentTest runs the real controller and UserSetRole on SQLite and MySQL/MariaDB. Details: Compare role reach by the grants that authorize at runtime #2578 (comment)

Full 3.6 suite in evo-ci-php:8.4: 2739 passed, 12 skipped. The new tests also pass on MySQL 8.4. PHPStan: 0 errors. The branch still merges into 3.6.x without conflicts.

@Seiger
Seiger merged commit bda6503 into evolution-cms:3.6.x Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants