Problem/Motivation
Wherever possible, Drupal 8 code should deprecate instead of removing old code, behaviors, etc.
Permission machine names are stored in user role configurations, but they are also used in code for many things like access checks. This means that if core updates the machine name of a permission, even if it provides an upgrade path, contributed module code that checks against the core permissions might start to deny access that should be allowed.
Marked major because it affects how thoroughly we can implement our backwards compatibility policy.
Proposed resolution
Provide a mechanism to deprecate permissions in the permission definition.
Remaining tasks
- Needs review.
- Followup to explore how to raise warnings when a deprecated permission is used.
User interface changes
If no deprecated permissions are used, there is no UI change: unused deprecated permissions are simply not presented to the user.
If a deprecated permission is in use, the user will see a short message under the permission. The permission can be revoked from roles that have it through the UI, but not granted to additional roles (unchecked checkboxes for it will be disabled).

A message will also be provided on the status report.

API changes
A deprecated key is added to the permission definition API.
Data model changes
| Comment | File | Size | Author |
|---|---|---|---|
| #64 | do-2924785--deprecated-permission-message.png | 34.5 KB | stborchert |
| #64 | do-2924785--permission-form.png | 154.42 KB | stborchert |
| #42 | interdiff-2924785-34-42.txt | 3.36 KB | robpowell |
| #42 | user-2924785-42.patch | 16.16 KB | robpowell |
| #34 | user-2924785-34.patch | 16.09 KB | xjm |
Issue fork drupal-2924785
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
xjmI'm thinking something like the attached.
I started to look at adding unit tests in
core/modules/user/tests/src/Unit/PermissionHandlerTest.php, but then backed away slowly because that test does very un-unit-testlike things (file_put_contents()of test permissions, etc...)Comment #4
xjmFor tests.
Comment #5
dawehnerCouldn't this cause security issues by people not seeing that a certain role has a certain permission. Maybe we should show it, but not make it possible to grant a role the permission?
🙃 These 4 lines could be collapsed to
return empty($permission['deprecated'])Comment #6
xjmAh yes. That's partly why I added the text about update hooks, but it's better to harden it against that. So we could show the checkbox if any role has the permission, disable the checkbox for any roles that don't have the permission, and display a message in the same place we add the restricted access message: "This permission should no longer be used".
derp yes. Booleans for our booleans!
Comment #7
xjmAttached does that. It includes a test fixture, but still no actual test.
Comment #8
sam152 commentedBased on #1977498: Add version tracking for configuration schema and data, permissions can't be renamed, even with an upgrade path right? Any exported role that contained such a permission in a module or elsewhere would be invalid.
I wonder if a follow-up for this could be a "replaced_by" key on deprecated permissions, so contrib modules which check the legacy permission could have the access check replaced by a lookup on the new permission automagically.
Comment #9
dawehnerCurrently the runtime permission system is fully decoupled from
hook_permissions, for example because of performance reasons. Maybe one thing we could do to keep this constraint alive is to update permissions automatically on role save.Comment #10
phenaproximaPatch looks good to me -- nice idea! Sending back to NW for tests, though :)
Comment #11
xjmRegarding updating permissions on save, this is something we try to avoid because it adds noise to config diffs.
We don't really guarantee that config exports will always work. We've had lots of upgrade paths that re-save config, generally accompanied by change records if key names/structure/etc. are changing. All those upgrades probably made old exports invalid.
I'm not sure about a
replaced_bykey or whatever, because permissions will not always have a 1:1 replacement. E.g. in #2862422: Add per-media type creation permissions for media one permission is replaced by many permissions.I was thinking that it would be up to the provider to maintain BC for the deprecated permission (and to add a
@trigger_error()for the deprecated permission in the relevant code paths) since only the module knows how the permission should be used. That's why the patch just hides them from the permissions UI.Comment #12
xjmNow with a test. This is also adding a test class that can eventually also test things from the very-un-unit-testlike PermissionHandlerTest.
Comment #13
xjmPermissionFormTest would probably be a better name.
I initially was going to add admin permissions here but decided it was out of scope, so the method name and docs need to be fixed accordingly.
This is like "If Nietzsche wrote a unit test."
Comment #14
xjmI guess we should also add here something like "The implementation is responsible for retaining backwards compatibility for the permission and for informing the developer of the deprecation."
Comment #15
xjmI could add another protected method for the checkbox selectors.
Comment #16
xjmWill post fixes tomorrow.
Comment #17
xjmUpdated test and docs.
Comment #18
xjmJust a couple test docs fixes etc.
Comment #19
xjmDuh.
Comment #20
xjmI drafted a change record in https://www.drupal.org/node/2926020.
I originally had this bit:
But that made me think the
@trigger_error()there is not actually helpful. If someone is checking the permission in their own access handler, then they're not going to necessarily exercise code paths in the core access control handler. And if they are exercising the code paths in the core access control handler then they probably also got the upgrade path.But to add the warning in
hasPermission()or such that would couple roles to the implementation of permission definitions, and run into the problem @dawhener alludes to in #9.It's sort of similar to the question of how we'd add warnings to deprecations for constants. So I think we should simply adopt a deprecation standard for permissions, allow the UI to support it, and maybe add a followup for how we might raise warnings about it.
Comment #21
xjmComment #22
xjmHmm, that also made me wonder about the upgrade path in #2862422: Add per-media type creation permissions for media which revokes the old permissions. If someone's custom access control handler was checking those permission names, following the upgrade path, it would always return 'access denied' and would be functionally the same for that code as if the permission had been removed entirely.
Comment #23
dawehnerlet's use strict=TRUE , as otherwise we run into https://3v4l.org/7qgqe potentially.
Nice!
I'm wondering whether it makes sense to name the permissions in a way that as a reader you already know which permission is used an which isn't. So 'deprecated used permission' and 'deprecated unused permission'.
Nice helper methods ...
Comment #24
xjmWTF @ https://3v4l.org/7qgqe... wow. Oh PHP.
Attached addresses #23.
Comment #25
xjmThat text will indeed never be displayed.
Comment #26
xjmDocblocks had a bit of copypasta.
Comment #28
dawehnerI really agree with the approach:
drupal_set_message(, 'warning')to make people aware,even if they just save the long form. What do you think about this?
For me its all about good method names, because this is what people actually most likely see.
Comment #29
xjmI've mixed feelings about a
dsm(). It could be bad UX to go to the form to grant Content Editors permission to create new revisions of My Whatsit Content Type, but to get a message out of nowhere Content Admins should no longer be granted the Update media permission. (Especially since the form is so long that it's hard to understand all the things that are there.)We could instead add a status report message or such so it's in the context of reviewing their site's status rather than updating something else.
Comment #30
dawehnerNow that you think about it, the existing test deprecation messages are sort of the same order than status messages. Given that sending the messages not always make sense.
+1 for adding a status report message as well?
Comment #31
xjmHere's that.
Comment #33
xjmIt does help if we actually test our patches.
Comment #34
xjmIt also helps if we don't leave our testing code in the patch.
Comment #36
phenaproximaLooks pretty great to me. I found nothing major that needs work here -- mostly just suggestions, nitpicks, etc.
$role_permission is a confusing variable name. It sounds like a singular thing, but it's an array of permissions in the role. Can it be renamed to $permissions_in_role or something like that?
I don't quite understand how this change is in scope...?
It might be prudent to only add this key if the permission is deprecated, so that it can properly pass isset() checks (for example, in hook_form_alter implementations).
Superdupernit: There is an extra space after {{ deprecated_warning }}, before the </em>. Not sure if that is intentional?
Because the deprecated_warning key is hidden behind the $hide_descriptions check, this will hide the deprecation warning if descriptions are turned off. Seems like we might want to show the deprecation warning unconditionally instead...
This should be strict-checked (TRUE as the third and final argument).
If the checkbox has or can have a title attribute, maybe we should add a little text to that attribute to explain why the checkbox is disabled ("This permission cannot be used because it is deprecated" or something).
s/ugprade/upgrade :)
Not a big deal, but I would suggest that this return the entire NodeElement associated with the row, which can allow future assertions to look for descriptions/checkboxes/etc. in the context of the element, rather than the entire page. (For example,
$row_element->find('css', 'div.description')). If you do this with assertSession(), you can also implicitly assert that the row, in fact, exists. So this method could look like:return $this->assertSession()->elementExists('css', static::rowSelector($name))Comment #37
xjmThanks @phenaproxima!
1 is not in scope; that variable already exists and is used in HEAD.
2 is in scope because otherwise, if the
access contentpermission is deprecated, the page will fatal with "unsupported operand type".3 is (again), following the existing pattern in HEAD which handles the restricted permission warnings in the same way.
For 5, I thought we should respect the UI pattern that is already used. Edit: Warnings about the restrict access permission are hidden in the same way and I think those are more important than this one. So I'd leave that out of the scope as well.
I'm not sure about 7; misuse of the title attribute is an accessibility issue. Edit: The other disabled checkboxes on the page for admin permissions don't have such a title so I don't think we should add it.
I'm confused by 9; it already does return the node element for the row?
I'll incorporate 4 and 6 after checking in about the other points. Thanks!
Comment #38
phenaproximaThat's true of $role_permissions (plural), but not $role_permission, which is introduced in the foreach loop in the patch. So we can totally give it a better name :)
That makes sense. Should we maybe open a follow-up to unconditionally display security/deprecation notices on permissions?
Makes sense.
Yes, it does, but it requires that the calling code assert that the return value is non-empty. Whereas assertSession()->elementExists() will do that for you implicitly, and return the element itself for further use. So we're doing extra unnecessary work in the current patch.
Comment #39
xjmAh I see. I think
foreach ($things as $thing)is usually better, even if$thingsisn't a great name itself. It allows the code to be read in the surrounding context. (If they'd given the overall variable a better name like$roles_permissionsor$role_permissions_mapthen the looping variable could actually have been called$role_permissionswhich sounds like singular role, plural permissions.) But I guess what it's definitely not is a singular permission, so renaming the loop variable probably makes sense.No strong opinion about this since I actually didn't even know about the "hide descriptions" feature in the first place, but a followup will at least mean we don't need to worry about it here.
Ah I get it now. Since elementExists() also returns the element itself, we can do that instead.
I'll reroll to incorporate those fixes. Thanks!
Comment #40
xjmObviously I didn't yet incorporate those fixes. :) Someone else can pick this issue up if they like!
Comment #41
robpowellComment #42
robpowellcompleted 1 and 9 from #36.
Making the change to
rowElement($name)had a tickle down effect todescriptionElement($name). This required three changes to the assertions asAssertEmpty()expected a null response.Finally, I also updated
checkboxElement($permission, $role)to also have an assertion rather than returning a null.Comment #43
robpowellComment #44
larowlanI think this is now too complex for an inline template, and we need a custom theme hook.
I don't think the implementation should be responsible for informing the developer, I think we should add code to
\Drupal\user\RoleStorage::isPermissionInRolesfor that and have it fire atrigger_errorAs mentioned on slack, I don't think these should be a boolean.
I think they should follow our normal deprecation format that way we can explain to people what to use instead.
In addition I think 'This permission should no longer be used' isn't very helpful - it doesn't tell people what to use instead or when they need to remove the usages by.
So by making this a string instead of a boolean, we could have it say
This permission was deprecated in Drupal 8.3.0 and will be removed before Drupal 9.0.0. Use 'Administer something' instead. See http://drupal.org/node/the-change-notice-nid.We can then use that same message in
\Drupal\user\RoleStorage::isPermissionInRoleswhen wetrigger_errorfor deprecated permissions. So that developers using the deprecated permission get feedback via tests that they're using a deprecated permission and know what to use instead.Comment #45
larowlanThis would require us to keep a cache of deprecated permissions for performance sake, as we don't want to be collecting permissions at runtime.
Comment #46
larowlanRelated #2339487: Static cache permissions
Comment #57
smustgrave commentedThis issue is being reviewed by the kind folks in Slack, #needs-review-queue-initiative. We are working to keep the size of Needs Review queue [2700+ issues] to around 400 (1 month or less), following Review a patch or merge request as a guide.
Yes this would be great feature!
At this time we will need a D10 version of the patch and there were changes requested in #44 that should be addressed
Please do not just reroll.
Comment #61
stborchertComment #63
stborchertI've tried to move the changes from the latest patch into a MR.
Things I could do:
deprecatedto permissions.yml that allows setting a deprecation message for a permissionUserPermissionsFormThings I couldn't do yet:
RoleStorage::isPermissionInRoles()is likely to be deprecated because it isn't used anymoreSetting to "Needs review" for some thoughts about the current progess.
Comment #64
stborchertComment #65
berdirDid a basic review.
On deprecations, I think you want to do a deprecation when a role with a deprecated permission is saved. We already have checks for non-existing permissions in \Drupal\user\Entity\Role::calculateDependencies, so the info is already available.
This will not cover code that uses it in hasPermission(), but we do not cache permissions atm, and checking it at runtime (access check time) would have way too much overhead. Doing it on role save matches the handling of non-existing permissions and deprecations are mostly for test, any test that tests a certain permission will ultimately need to set it on a role for it to do anything.