We have quite some special behavior for uid #1. This tends to confuse the user, and will do so even more now we have an admin role in core. Do all the special cases we designed around uid #1 really make sense?

Problems with special casing uid 1

  • People are confused whether they should be sharing credentials of this user (bad) or just using a user with the Administrator role.
  • The lack of permission check on user 1 means that uid 1 can take actions on a site even if they do not have a role that grants them the permission. This makes it hard to completely disable unwanted features on a site unless you block uid 1 and never use it.
  • The lack of permission check on user 1 makes them a particularly valuable account for an attacker to take over in some way. Any time there is a focus point for an attack it makes the system weaker.

Potential problems with *not* special casing

If someone is playing around and removes all roles or removes all permissions from all their admin users it could be possible to have no users with administrator permissions on a site. While is is a problem it is easily solved with a FAQ page and some database instructions just like we do with WSOD.

Also, because of the long legacy of this in core, both contrib modules and custom code in individual sites might (will, in some cases, see #175) have adopted similar code to grant uid=1 special privileges. When core no longer makes any assumptions about the specialness of uid=1, site maintainers may now strip uid=1 from its admin role, thinking it is now a normal user, only to have hard-coded god-funcitonality lingering around their site.

Proposed resolution

The SuperUserAccessPolicy is put behind a container parameter which keeps the policy on by default for D10. In D11, we either turn it off by default as user 1 gets the Administrator role in standard_install() or we completely remove the SuperUserAccessPolicy.

By that point (the removal) we should, however, have made a recovery.php which is behind a flag in the settings file similar to update.php (update_free_access). If someone locks themselves out of their website, we can use said script to assign the Administrator role to a user of their choosing. if the Administrator role no longer exists, the script will create one. This script should be developed in a follow-up

Remaining tasks

  1. Put the SuperUserAccessPolicy behind a parameter on the container - DONE
  2. Toggle this off for all browser and kernel tests - DONE
  3. Find all failing tests and flag them - DONE
  4. Create an issue #3437620: [Meta] Fix all tests that rely on UID1's super user behavior - DONE
  5. Create a followup to create recovery.php and remove the toggle along with the access policy

API changes

None

Release notes snippet

User 1's special privileges are now part of the SuperUserAccessPolicy, which can be turned off. From Drupal 10.3.0 onwards, you can toggle this behavior in your default.services.yml file by setting the container parameter security.enable_super_user to false.

CommentFileSizeAuthor
#291 Screenshot 2021-02-20 at 20.39.18.png154.44 KBeelkeblok
#291 Screenshot 2021-02-20 at 20.38.21.png183.64 KBeelkeblok
#276 interdiff.540008.271-276.txt1.6 KBabhisekmazumdar
#276 540008-276.patch70.02 KBabhisekmazumdar
#271 540008-271.patch70.06 KBdaffie
#271 interdiff-540008-265-271.txt2.59 KBdaffie
#265 540008-265.patch69.69 KBdaffie
#265 interdiff-540008-263-265.txt1.66 KBdaffie
#263 540008-263.patch69.8 KBdaffie
#263 interdiff-540008-250-263.txt1.72 KBdaffie
#256 no-results.png140.46 KBkristiaanvandeneynde
#250 540008-250.patch68.82 KBdaffie
#250 interdiff-540008-248-250.txt5.73 KBdaffie
#248 interdiff_222-248.txt8.45 KBjohnwebdev
#248 540008-248.patch66.35 KBjohnwebdev
#228 540008-222.patch69.5 KBdaffie
#227 interdiff_222_225.txt4.12 KBSpokje
#225 540008-225.patch71.87 KBNitinLama
#225 interdiff_222_225.txt3.25 KBNitinLama
#222 540008-222.patch69.5 KBSpokje
#222 interdiff_218_222.txt5.48 KBSpokje
#218 interdiff_216_218.txt3.12 KBanmolgoyal74
#218 540008-218.patch72.26 KBanmolgoyal74
#216 540008-216.patch72.83 KBSpokje
#216 interdiff_214_216.txt5.23 KBSpokje
#214 540008-214.patch68.01 KBSpokje
#214 interdiff_210_214.txt5.73 KBSpokje
#213 540008-213.patch68.01 KBSpokje
#213 interdiff_210_213.txt5.73 KBSpokje
#210 540008-210.patch65.06 KBSpokje
#210 interdiff_209_210.txt2.49 KBSpokje
#209 540008-209.patch62.64 KBSpokje
#209 interdiff_208_209.txt3.62 KBSpokje
#208 540008-208.patch61.45 KBSpokje
#208 interdiff_207_208.txt7.52 KBSpokje
#207 540008-207.patch53.59 KBSpokje
#207 interdiff_206_207.txt689 bytesSpokje
#206 540008-206.patch53.53 KBSpokje
#206 interdiff_202_206.txt2.06 KBSpokje
#203 540008-202.patch53.68 KBSpokje
#203 reroll_diff_169_202.txt38.3 KBSpokje
#169 interdiff-169-168.txt1.01 KBkristiaanvandeneynde
#169 drupal-540008-169.patch52.97 KBkristiaanvandeneynde
#168 drupal-540008-168-do-not-test.patch52.21 KBkristiaanvandeneynde
#162 interdiff-162-156.txt1.24 KBkristiaanvandeneynde
#162 drupal-540008-162.patch71.39 KBkristiaanvandeneynde
#156 interdiff-156-152.txt5.15 KBkristiaanvandeneynde
#156 drupal-540008-156.patch70.41 KBkristiaanvandeneynde
#152 interdiff-152-151.txt2.38 KBkristiaanvandeneynde
#152 drupal-540008-152.patch67.25 KBkristiaanvandeneynde
#151 interdiff-149-144.txt1.18 KBzaporylie
#149 interdiff-149-144.txt7.88 KBzaporylie
#149 remove_the_special-540008-149.patch63.92 KBzaporylie
#144 interdiff-144-134.txt7.48 KBkristiaanvandeneynde
#144 drupal-540008-144.patch66.73 KBkristiaanvandeneynde
#134 interdiff-134-131.txt6.71 KBkristiaanvandeneynde
#134 drupal-540008-134.patch66.14 KBkristiaanvandeneynde
#131 interdiff-131-129.txt6.49 KBkristiaanvandeneynde
#131 drupal-540008-131.patch63.34 KBkristiaanvandeneynde
#129 drupal-540008-129.patch57.11 KBkristiaanvandeneynde
#101 interdiff-99-101.txt1.93 KBkristiaanvandeneynde
#101 drupal-540008-101.patch57.12 KBkristiaanvandeneynde
#99 interdiff-98-99.txt1.6 KBkristiaanvandeneynde
#99 drupal-540008-99.patch56.43 KBkristiaanvandeneynde
#98 interdiff-89-98.txt2.18 KBkristiaanvandeneynde
#98 drupal-540008-98.patch55.08 KBkristiaanvandeneynde
#89 drupal-540008-89.patch55.06 KBkristiaanvandeneynde
#89 interdiff-86-89.txt5.17 KBkristiaanvandeneynde
#86 drupal-540008-86.patch50.17 KBkristiaanvandeneynde
#86 interdiff-78-86.txt1.18 KBkristiaanvandeneynde
#80 interdiff.txt7.62 KBmichaelfavia
#80 540008-uid1.patch53.82 KBmichaelfavia
#79 user-1-edit.png63.01 KBkristiaanvandeneynde
#79 roles.png126.6 KBkristiaanvandeneynde
#79 permissions.png35.62 KBkristiaanvandeneynde
#79 admin-people.png69.91 KBkristiaanvandeneynde
#79 account-settings.png113.79 KBkristiaanvandeneynde
#78 drupal-540008-78.patch49.24 KBkristiaanvandeneynde
#78 interdiff-76-78.txt3.24 KBkristiaanvandeneynde
#76 drupal-540008-76.patch46.63 KBkristiaanvandeneynde
#76 interdiff-74-76.txt2.68 KBkristiaanvandeneynde
#74 drupal-540008-74.patch47.66 KBkristiaanvandeneynde
#74 interdiff-70-74.txt5.91 KBkristiaanvandeneynde
#70 interdiff-67-70.txt5.86 KBkristiaanvandeneynde
#70 drupal-540008-70.patch44.45 KBkristiaanvandeneynde
#67 drupal-540008-67.patch38.9 KBkristiaanvandeneynde
#67 interdiff-65-67.txt6.96 KBkristiaanvandeneynde
#65 interdiff-62-65.txt13.84 KBkristiaanvandeneynde
#65 drupal-540008-65.patch34.05 KBkristiaanvandeneynde
#62 drupal-540008-62.patch21.65 KBkristiaanvandeneynde
#49 interdiff-47-49.txt1.41 KBkristiaanvandeneynde
#49 drupal-540008-49.patch22.7 KBkristiaanvandeneynde
#47 interdiff-45-47.txt14.88 KBkristiaanvandeneynde
#47 drupal-540008-47.patch22.36 KBkristiaanvandeneynde
#45 interdiff-42-45.txt5.05 KBkristiaanvandeneynde
#45 drupal-540008-45.patch10.84 KBkristiaanvandeneynde
#42 drupal-540008-42.patch6.08 KBkristiaanvandeneynde
#21 540008-21-remove-special-user-1.patch3.02 KBianthomas_uk
#19 540008-19-remove-special-user-1.patch1.1 KBianthomas_uk
#13 540008_stop_special_casing_user_1.patch5.99 KBgreggles
#4 user1sansmagic.patch5.4 KBmichaelfavia

Issue fork drupal-540008

Command icon 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:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

timmillwood’s picture

Do you think we should remove uid #1's specialness, and just give uid #1 the admin role on registration?

Bojhan’s picture

Yes.

michaelfavia’s picture

I was about to roll a patch and create an issue for just this case. Ill work something up before BADCamp and see if i can get anyone there to review it. Subscribe.

michaelfavia’s picture

Now that Drupal automatically adds user #1 to the administrator role we dont have to do it ourselves to make this happen.

We probably need to add permissions for the special handling that some of core uses presently. For now i have removed the lines in the patch so it should be fairly obvious what needs to be added back:

  1. Block caching was bypassed for user 1. Do we have or want a permission for this or should it just respect drupals defaults as it in the patch as it stands?
  2. hook_file_url_alter() always served user 1 local files instead of passing to cdn. Now they are a regular user should we add a permission for this "feature" or does one exist?
  3. User 1 was prevented from accidental or intentional deletion. How would we like to handle this now that user 1 has no special powers?
    We can ensure that at least one user with the "administrator role" remains but that role can be stripped of "administer users" and "administer permissions" permissions. To prevent lockout we can either lock those permissions to "on" like is done with comment approvals or just test against the permissions we care about and ignore the role altogether. Thoughts?

The attached patch is only a starting point and removes all of the references to user 1 that i know off the top of my head or could find with a simple grep but doesnt address the issues raised above. If we find consensus on how to treat each above ill add it to the patch.

AaronBauman’s picture

Version: 7.x-dev » 8.x-dev

Too late for 7.x

michaelfavia’s picture

Status: Active » Needs review

Sorry about that i even notice it was tagged for 7.x still. the patch was against 8.x queuing for testing.

Status: Needs review » Needs work

The last submitted patch, user1sansmagic.patch, failed testing.

kika’s picture

Alternative: create new "superuser" role (all permission boxes set), assign it to uset 1 with one exception: he should not be able to lock himself out of the permission-setting page.

Note that removing uid superuser powers cripples new module install experience, uid 1 needs to remember to set modules permissions first to access it's admin screens and functionality, then try it out and then set the permissions for the rest of the users.

michaelfavia’s picture

kika: the point (imo anyway) of this was to not change the user 1 experience in any fashion but remove the special casing of that user so that more users could inhabit that role. THis way people dont have to share credentials, etc.

Everywhere in drupal permissions are handled by roles except in this one case (user 1). This issue squares that circle for us. It's a usability win if my clients are any indication of the normal users of drupal.

With that in mind i would agree with you that new permissions should be added to the "administrator role" as they are made available by enabled modules (i think they already are?) and that we should prevent the administrator role from removing any permissions or at least the "administer permissions" permission. This last one is an open question but id prefer this be a role equivalent to the super user so locking all permissions on works for me personally.

Finally if we prevent the "last remaining user" of the "administrator role" from being unassigned, or deleted then we should be pretty well covered from people locking themselves out unintentionally.

@all: Sound like a reasonable approach for my next version of the patch?

webchick’s picture

Marked #130213: Stop assuming userid=1 is superadmin as a duplicate of this issue. It was technically older, but this one has a patch.

greggles’s picture

Issue tags: +Security improvements

This would also help improve security, IMO.

And subscribing.

chx points out in irc that if uid 1 is not special cased it would be important to provide a way to let someone back in if they remove all roles on uid 1. I don't consider that to be a big problem. It's a handbook page with 2-3 queries to use to identify what kind of lockout you have and fix it.

tstoeckler’s picture

Alternatively we could add a form validation handler that checks if you're the last user with the admin role and and stops removing that role in that case. That would handle stuff like VBO role updates, but it would still stop people from "locking themselves out".

greggles’s picture

For accidentally removing all roles, we already have some code to prevent that issue:// Prevent user administrators from deleting themselves without confirmation. in UserMultipleCancelConfirm.php. And, the admin role already gets all new permissions for people using a profile that creates it. I don't think we should do anything more than what we already do. If there are weird cases where people get locked out we can have a doc page OR fix this problem when we are sure that it's a problem.

Attached is a reroll of #4. I then grepped core for "= 1" and "ser 1" and "ser #1". I mostly found nothing that looked important except for UserSession.php hasPermission which was hardcoding TRUE all permissions for uid 1.

Let's see what testbot thinks.

greggles’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 540008_stop_special_casing_user_1.patch, failed testing.

joachim’s picture

> now we have an admin role in core

In my experience on D7, the admin role does not work perfectly, and does not get all permissions granted to it.

Also, presumably an admin needs to be able to administer permissions. So even if you did limit the permissions of uid 1, any attacker could just add them back? Or is the intention that the site is locked down so nobody can admin permissions?

greggles’s picture

Bugs in the admin role should be filed as bugs in the admin role :)

In addition to the usability improvement that started this issue, there are two specific security-related cases that I care about:

1. Paranoia module hides some permissions in the permissions page and ensure that they are not granted with a custom form validate function. An attacker who gets access to a role with administer permissions is still unable to grant those roles to themself. But if UID1 always gets all roles, that protection is not helpful.
2. I've seen modules that use permissions that are not defined in a hook_permission as a way to make the feature usable only by UID 1. In general these are really risky things. That seems like a bad practice to me that we should prevent from being used.

I don't really understand why there are all these test failures. I guess the tests rely on uid1 bypassing all permission checks?

greggles’s picture

Issue summary: View changes

summarize discussion to date

catch’s picture

Priority: Normal » Major
Issue summary: View changes
Issue tags: +D8 upgrade path

This still seems like a good idea, and there's even less reason for uid 1 special casing now update.php is a route.

Bumping to major, tagging D8 upgrade path since we might need to add a 'run updates' permission and/or add the admin role to uid 1.

ianthomas_uk’s picture

Status: Needs work » Needs review
FileSize
1.1 KB

Here's an updated patch that just removes the extra permissions from user 1 so we can see what tests will fail.

The other hunks in #13 are either about prevention deletion or disabling block caching. Maybe we still want to do this, but based on a permission instead of uid (at least the deletion, not sure about the caching).

Status: Needs review » Needs work

The last submitted patch, 19: 540008-19-remove-special-user-1.patch, failed testing.

ianthomas_uk’s picture

Status: Needs work » Needs review
Issue tags: +Novice
FileSize
3.02 KB

I imagine we can solve a lot of the test failures by replacing use of WebTestBase::rootUser and instead creating a user with the necessary permissions. This fixes the first failure, BlockHtmlTest, and would make a good novice task for someone to go through the other tests and adding the appropriate permissions.

Standard permissions are:
administer blocks
administer modules
administer site configuration
administer software updates
administer themes
link to any page
access administration pages
access site in maintenance mode
access site reports
view the administration theme
administer account settings
administer permissions
administer users
cancel account
change own username
select account cancellation method
access user profiles

Status: Needs review » Needs work

The last submitted patch, 21: 540008-21-remove-special-user-1.patch, failed testing.

finne’s picture

Assigned: Unassigned » finne

I would like to look into this issue at the DDD montpellier today.

finne’s picture

I encountered the folling when testing:

A) After patch user 1 cannot access test in the GUI. I needed to create an administrator role and give it all privileges, and set it as the admin role in admin/config/people/accounts, and give user 1 this administrator role. This is a logical consequence of removing the special treatment of user 1.

B) Because of this the Drupal install script that creates user 1 also needs to enable administrator privileges for user 1. This needs to be done as described in point A: by creating a user role, setting that user role to be the administrator user role, and then enable all privileges for all modules that were already enabled at that time (assuming drupal will assign those privileges for any module enabled afterwards). The created user (1) then needs to be assigned the created administrator user role.

C) In WebTestBase the rootUser is assigned uid 1. Here I would suggest the same procedure as on install needs to be followed: create role, assign role admin status, assign all privileges to role, assign role to user.

Any thoughts on this solution? If it looks like a good solution I will proceed to implement it.

Berdir’s picture

I think it's time to discuss this issue with a core committer to check if this change is still doable in 8.x. It looks like we would need considerable changes, for example, minimal has no administrator role right now, only standard has, so we would have to move that out of the install profile and create one by default in user.module. And we would need dozens of fixes in tests.

The first thing that you for that is a beta evaluation in the issue summary that explains why this issue is still commit-worthy.

ianthomas_uk’s picture

re #24 a/b, it was mentioned in #4 that user 1 is given the administrator role. I've not tested that, but I didn't have to add any extra roles.

In WebTestBase the rootUser is assigned uid 1. Here I would suggest the same procedure as on install needs to be followed: create role, assign role admin status, assign all privileges to role, assign role to user.

That will work and is probably an easier fix, but I thought it was good to document and test the permissions that are required, which happens when you use the pattern I did in #21.

Berdir’s picture

re #24 a/b, it was mentioned in #4 that user 1 is given the administrator role. I've not tested that, but I didn't have to add any extra roles.

Yes. In standard profile. But not for minimal and not for testing or any other install profile that someone else would create.

It's also very easy to accidently delete that role or remove uid 1 from that role, and then your site would be completely broken if you don't have another user with that role.

I'm not sure I see the security improvements there :)

Also, note that the admin role now has *all* permissions as well, just like uid 1. So there really is no difference anymore between the two.

I don't think we should make this change.

greggles’s picture

@Berdir, thanks for your feedback.

You said:

Also, note that the admin role now has *all* permissions as well, just like uid 1. So there really is no difference anymore between the two.

There is at least one big difference: uid 1 always has all permissions. If a site admin removes a permission from the admin role then uid 1 will still have that permission. For example, the paranoia module removes all permissions from the permission form that are so risky they shouldn't be granted to anyone in a well-secured site (e.g. use PHP for settings'). But uid 1 still has the ability to do things behind that permission. On Drupal.org specifically, which does run the Paranoia module, this behavior of uid 1 makes me rather scared (and puts more responsibility in Dries to properly manage his account than is fair to him).

It's also very easy to accidently delete that role or remove uid 1 from that role, and then your site would be completely broken if you don't have another user with that role.

There are a lot of ways to "shoot yourself in the foot" and we should make it hard to do them and we should make it easy to recover from them. But the examples you talk about are no worse than the others and the benefits of this issue outweigh the usability decrease.

ianthomas_uk’s picture

Another disadvantage of UID 1 is that it only applies to one account, so it's harder to keep track of who knows the password to your super account than if you give everyone their own, named account and disable people who should no longer have access.

We currently have checks to stop you deleting UID 1. We should replace/extend those checks to stop people deleting the last user with the administrator role, or something along those lines (probably a lot harder in practise than in principle). We can also look at methods of recovering that access.

Even if we decide not to do this for Drupal 8, I still think it is worth changing the tests, so that they are documenting and testing the permissions required.

Berdir’s picture

No, that difference no longer exists. the admin role is now a flag on the role, if set, then it has all permissions, just like uid 1.

I see the security implications of that, but a) That resolves a huge UX issue that the administrator role was missing a lot of dynamic permissions and b) solves performance issues where we have to storage and load a huge list of permissions and update them when we install modules.

greggles’s picture

I see that was added in #2435075: Implement admin role as a flag on the role storage, simplify permissions page, remove user_modules_installed.

That change feels like a real bummer for me. I understand how that change solved a bug, but it seems it's solved in a suboptimal way from a security perspective.

I filed #2471763: Ensure no roles are "admin" roles (8.x) to deal with it.

finne’s picture

Issue summary: View changes
finne’s picture

I updated the issue summary and added a beta evaluation. Do I need to tag the issue so it is flagged for evaluation?

Re: #26: I agree that it would be a good idea to test with the proper permissions, instead of the blanket admin user. This might be added as a separate issue, and also added to the test documentation.

Patrick Storey’s picture

Issue tags: -Novice

I am removing the Novice tag from this issue because it appears this issue may be shifting direction and is lacking a consensus on the next steps.

I’m using this documentation as a source: https://www.drupal.org/core-mentoring/novice-tasks#avoid

michaelfavia’s picture

Removing the special casing of UID one is still valuable in my opinion. We handle absolutely all other permissions by checking against assigned roles. The UID = 1 is a special case and an great example of a drupalism that makes things more confusing for new developers to the system and introduces special cases in code throughout the logic of drupal. We are just used to it.

Using our shop as an example it causes trouble because people (customers and devs) understandably seek out UID 1 (via drush uli or similar) instead of embracing their own user with the appropriate admin role to make modifications on client sites. As a result you cant ever trust who was making what changes in drupal logging infrastructure because it was most likely someone "sudoing" in drupal. Making all users with the role equal wold remove the impetus to do that.

Whether through sharing UID1 passwords or removing an audit log it continues to cause trouble and confusion.

Xano’s picture

The primary reason for having a sudo user seems to be development and low-level maintenance. However, I do not presume I can predict how other developers use user 1 on the sites they build, so removing the user entirely could be too radical. We could add a site setting or configuration item to explicitly (dis)allow user 1 to be active on the site. It can be disabled for new installs and enabled for sites that were upgraded from Drupal 7. This would allow developers and site builders to navigate the site using user 1 during the development phase, yet allow the account to be disabled on staging and production sites, forcing people to use individual, more restricted accounts (as described in #35).

michaelfavia’s picture

@Xano: any reason a developer couldn't just:
drush urol administrator UID

My understanding is that the whole point of this was to simplify the permission handling from a DX and UX perspective and I'd hate to see another edge case get introduced. This has the added benefit of leaving an audit trail in the logs for THAT specific developers actions in the system instead of "somebody sudoing as UID 1".

I think it can and should be very simple to explain to people (new users and confused new developers): Your permissions on a drupal install are the aggregation (bitwise OR) of the roles your user is assigned. Its easy to explain and it makes sense.

mgifford’s picture

Assigned: finne » Unassigned

Unassigning stale issue. Hopefully someone else will pursue this.

catch’s picture

Version: 8.0.x-dev » 8.2.x-dev

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

kristiaanvandeneynde’s picture

Reviving this issue with a very basic patch that has a slightly different approach.

Instead of creating an admin role that can be assigned to others, it creates a superuser role which will not be assignable through the UI or code. Only user 1 will ever get it. This is essentially the same as suggested in the issue summary, but with full backwards compatibility. It will also unblock the following issue: #2749865: Rename the 'user.permissions' context to 'user.roles.permissions', to ensure correct cache context optimization

Issues that may be closed if/when this lands:

Will adjust the patch after CI has had a go at it. It still might need some UI clean-up and test failure fixes.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 42: drupal-540008-42.patch, failed testing.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
Issue tags: -D7UX, -D8 upgrade path
FileSize
10.84 KB
5.05 KB

Still expecting some test failures, for instance RenderCacheTest needs to be rewritten entirely or simply removed.

Status: Needs review » Needs work

The last submitted patch, 45: drupal-540008-45.patch, failed testing.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
FileSize
22.36 KB
14.88 KB

Fixed a few more test fails. This interestingly shows how much our tests and code rely on UID 1 having certain benefits. One test failed because UID was incorrectly created as blocked (status = 0), but the test still worked because of a UID == 1 test, regardless of blocked status.

Status: Needs review » Needs work

The last submitted patch, 47: drupal-540008-47.patch, failed testing.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
FileSize
22.7 KB
1.41 KB

Okay so there was a bug in User::preSave() which this patch uncovered. It was calling $this->get('roles')->offsetUnset($index); while inside of a loop. But the thing is, ItemList::offsetUnset() actually re-indexes the value list, causing an exception to be thrown when you try to offset an index that no longer exists.

See interdiff for details. Curious to see how many tests will still fail now.

michaelfavia’s picture

@kristiaanvandeneynde: Thanks for taking this on. I recognize that fixing this issue might be a bit of an undertaking . My only concern with the approach you outline above is that it introduces yet another drupalism that special cases permissions when my original intent was to remove one altogether.

Most decent systems I know of these days that deal with multiuser access don't attempt to ordain special users via code and instead allows sufficiently privileged users to assign rights to others either through roles, groups or directly to the user itself in the UI. I hope we all agree at this point that treating users in this generalizable way is much more transparent for users and developers alike.

After a quick review it really seems like your approach here gets us most of the way to just programmatically creating and assigning this role to user 1. What stops us from making it the administrative role and disabling its deletion like anonymous and authenticated?

Thanks again!

kristiaanvandeneynde’s picture

What stops us from making it the administrative role and disabling its deletion like anonymous and authenticated?

Nothing. The only thing I am worried about is that if we were to make it an actual role that has to be assigned to users explicitly, people will lock themselves out of their own website.

There is one alternative: To call the role Administrator instead of Superuser and make it assignable to other people but irrevocable from user 1. The only downside I see here is that once we go down this road, you just know someone will bypass the API and revoke the role from user 1 through the DB. If we keep the role special and implicit, it cannot be revoked without hacking core.

At least with the patch as it stands now, we have one Drupalism fewer with regard to permissions. We'd be actually using our "admin role" system for user 1 from now on.

Status: Needs review » Needs work

The last submitted patch, 49: drupal-540008-49.patch, failed testing.

drakythe’s picture

The only downside I see here is that once we go down this road, you just know someone will bypass the API and revoke the role from user 1 through the DB. If we keep the role special and implicit, it cannot be revoked without hacking core.

As it currently stands you can delete or otherwise FUBAR user 1 in the DB, I don't think we should shy away from a solution just because the DB could mess it up.

If we wanted to, we could go the Administrator role route, lock it like anonymous and authenticated, and then add a special role deletion to Administrator so anytime it was removed from any user (whether the initially created user 1 or a later updated user) there would be an extra confirmation step making the user aware that if all users have the role removed then they may lose access to certain functionality on their site and require DB access to fix.

Perhaps we could write a Drush function to re-apply the role to UID 1? Once a user has commandline access they pretty much have the site, so I don't think that would be a security issue.

greggles’s picture

To call the role Administrator instead of Superuser and make it assignable to other people but irrevocable from user 1.

This strikes me as being contrary to the purpose of this issue. The UID 1 user, especially on old sites, may be an "every day" account. It is hard to migrate away from that practice. A person's responsibilities can shift over time (e.g. a person creates a Drupal site so they are uid 1, they write blog posts and edit content and administer the site, this works fine for a while, their company grows and now is an e-commerce business, they are the CEO and should not have every permission on the site as part of PCI Compliance and common security best practice but Drupal forces their account to have these extra permissions).

We should enable people in that scenario to revoke the "special" extra permissions from their uid 1. This might lead to some people getting "stuck" if they do the wrong thing and then they can fix it in the DB or via drush/console or something. That is OK. The original motivation for this issue was UX, so if Bojhan says "let's do this for UX" we should not perpetuate the special-casing of UID1/Roles just because we're concerned about the UX implications of a corner case.

michaelfavia’s picture

I'd like to take a shot at simplifying the requirements and expectations that I hope we can all agree on at this point. If we agree on the following I'll update the issue summary to reflect them. I'd love to have @Catch's opinion because ultimately we need this in core or its all for naught.

Assigned roles, not user special casing should be where a users permissions are derived from. In practice this means removing the "$user->uid == 1" checks. All permissions should be tested in the usual way without exception for UID. An administrator role that is not editable, undeleteable and always returns true for hasPermission() should be created for this purpose.

While we don't technically NEED for at least one user in the system to have that administrator role, requiring it on the UI side would provide a certain amount of bulletproofing for our users and is a reasonable default. To accomplish that we need to check to make sure at least one other user has that permission when you are unassigning user permissions or deleting users.

This administrative role should be automatically granted to the first user of the site on site creation but should remain removable from that user as long as some other user still has it.

If the user edits the database its all up to them to make the world work. If they can delete permissions in the DB they can re-add them. This is consistent with the make easy things easy and make hard things hard philosophy. This same thought process would allow a determined user to actually physically delete the last administrative user if they really wanted to for some arcane technical reason and has the sufficient skills to do so.

Are we all on the same page?

drakythe’s picture

Sounds reasonable to me. Though I'd add that the administrative role should not be able to be deleted, like anonymous and authenticated.

michaelfavia’s picture

Agreed 100%. Sorry if I was ambiguous but I assumed non-editable covered that. Adjusted to undeletable as well ;).

drakythe’s picture

You know, if I hadn't somehow magically skipped that line while reading your summary it probably would have covered that for me, but more clarity doesn't (usually) hurt.

greggles’s picture

+1 to #55.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review

Adjusting the patch to meet #55 then. Stay tuned.

kristiaanvandeneynde’s picture

Status: Needs review » Needs work
kristiaanvandeneynde’s picture

Status: Needs work » Needs review
FileSize
21.65 KB

No interdiff due to radical changes, expecting some test failures again. Note that this patch still contains the bugfix for the bugs found in #47 and #49. Expecting a ton of test failures.

catch’s picture

#55 sounds good to me, I think I like the validation more than special casing uid 1 in the UI (although I'm not completely opposed to that either, you could still remove the role via drush if we did).

The central point of 1. create the new role 2. don't let it be deleted 3. assign it to the first user on the site sounds great though.

Tagging for usability review. Screenshots of the validation message (and how the role checkboxes look on uid 1 etc.) would be useful for that.

Status: Needs review » Needs work

The last submitted patch, 62: drupal-540008-62.patch, failed testing.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
FileSize
34.05 KB
13.84 KB

Well this just goes to show how many tests are currently actually broken because of some happy accident making the user they created UID 1.

  1. CommentDefaultFormatterCacheTagsTest never meant to test with UID 1 but did, so the cache tags are different. Please let me know what the actual cache tags should be.
  2. CommentViewsKernelTestBase never meant to use UID 1 but did, so it worked with too few permissions. Added 'access comments' and 'post comments'. Please confirm if this is the right fix.
  3. Had to refactor AccountPermissionsCacheContext to stop special casing UID 1 and as a result PermissionsHashGeneratorTest

Here goes to another CI round :)

Status: Needs review » Needs work

The last submitted patch, 65: drupal-540008-65.patch, failed testing.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
FileSize
6.96 KB
38.9 KB

Further fixes of wrong access checks (mainly having UID 1 by accident). UserViewsFieldAccessTest was a doozy because it was actually trying to read out user properties such as 'status' which are explicitly forbidden by UserAccessControlHandler. So I removed all of the offending properties from that test.

Funny how many tests we have that were incorrectly passing because of the UID 1 thing. Will fix the last few failing tests next week. Then I need to write an update hook to add the role and perhaps a test that verifies if user 1 gets the admin role on install.

Status: Needs review » Needs work

The last submitted patch, 67: drupal-540008-67.patch, failed testing.

michaelfavia’s picture

Excellent work @kristiaanvandeneynde. Glad you were able to expose the tests too. Yet another good reason to get this into core. ;) Thanks for your effort and ill try and load this up this weekend or early next week to test and take a look myself as well.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
FileSize
44.45 KB
5.86 KB

FieldFieldTest flat out relied on UID 1 being special. If not, the whole test broke because it tried to access the forbidden timezone property (see above).

CommentDefaultFormatterCacheTagsTest is failing because it used to run as UID 1 who was shown far more cache tags. Probably because of all the edit links. Right now I've already fixed the first comparison which should only show the cache tags for the actual entity.

The second comparison only succeeds if the user has the 'administer comments' permission. I have no clue why user:2 and comment:1 are added only then. Because when changing a user's name, the label in the comment is updated for everyone. Even people who can't administer comments.

RowRenderCacheTest is very much broken as explained in #2473873: Views entity operations lack cacheability support, resulting in incorrect dropbuttons.

It used to use the following cache contexts:

  • UID 1: user.permissions
  • UID 2: user
  • UID 3: user

But after the UID 1 normalization it uses:

  • UID 1: user
  • UID 2: user
  • UID 3: user

The funny thing is: The test checks for the 'edit any test content' permission which is never assigned to anyone. Anyone with the permissions gets the 'user.permissions' context instead of 'user'. So UID1 got a different context which for some odd reason made the test go green. Reason is probably explained better in the issue I linked.

By currently giving everyone user.permissions just like in ::testNoCaching(), the test still goes green. So I have done that. The people working on #2473873: Views entity operations lack cacheability support, resulting in incorrect dropbuttons can take care of properly fixing that test.

Status: Needs review » Needs work

The last submitted patch, 70: drupal-540008-70.patch, failed testing.

kristiaanvandeneynde’s picture

So it's just CommentDefaultFormatterCacheTagsTest failing now because of the weird cache tags I can't quite get my head around.

After that, this patch needs:

  • An update hook to add the role to sites that don't have it and assign it to their UID1
  • A test to confirm that on any install, UID gets the administrator role
  • A user constraint that disallows the last admin user from having their admin role stripped. Perhaps for better UX we could disable the checkbox on forms, but a constraint sounds better as it also works with other layers of the API
  • Make sure this is also true when bulk updating users through the UI
Wim Leers’s picture

It's not immediately clear why so many cache tags are no longer showing up when uid=1 in CommentDefaultFormatterCacheTagsTest. When the comments are being rendered in CommentViewBuilder extends EntityViewbuilder, it's \Drupal\Core\Entity\EntityViewBuilder::getCacheTags() that e.g. generates the comment_view cache tag that is currently missing. Stepping through comment rendering with a debugger as uid=1 should allow you to find the root cause.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
FileSize
5.91 KB
47.66 KB

I found it! :)

The comment_install() function never runs during the test which does not set the state entry that tracks comment count. Without that, only admins can see the rendered comment field, which explains the massive lack of cache tags. Running comment_install() and granting the user the 'post comments' permission fixed things.

Also: Thanks for answering my call to look into this, Wim.

Status: Needs review » Needs work

The last submitted patch, 74: drupal-540008-74.patch, failed testing.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
FileSize
2.68 KB
46.63 KB

We actually need the add/remove administrator role actions. Also the administrator role will show up as such in the UI. Should be close to if not at 0 failing tests now.

kristiaanvandeneynde’s picture

Excellent! So now the question becomes: Where do we go from here?

We could add UI limitations and/or constraints to prevent the removal of the admin role from the final admin user. Or we could go with the suggestion in the issue summary outlined under "Potential problems with *not* special casing". I.e.: Create a page with instructions as to how we can restore access to a user when all admins are gone.

I'll play around with the patch on simplytest.me later on to see what it looks like in the UI right now. I'll also add a check to UserInstallTest to check for the admin role and an update hook to add it to existing sites.

kristiaanvandeneynde’s picture

FileSize
3.24 KB
49.24 KB

This patch adds an update and post-update hook for making sure the role exists and UID 1 has it. It also extends UserInstallTest to check whether UID 1 gets the role upon site install.

The only part I'm worried about is that the update hook grants is_admin to the administrator role on existing sites. If anyone for some reason turned that flag off, they will suddenly have it again. Probably no-one would have an 'administrator' role without admin privileges, but still mentioning it.

kristiaanvandeneynde’s picture

FileSize
113.79 KB
69.91 KB
35.62 KB
126.6 KB
63.01 KB

These screenshots were taken after a fresh install on simplytest.me. Note that UID 1 can remove his own admin role and currently the UI shows a delete action in the dropbutton for the admin role as well.

  • Whether or not we want to babysit websites into not removing the admin role from the last admin is still up for discussion.
  • We can make sure the delete action does not show by adding the administrator role ID to RoleAccessControlHandler::checkAccess. It depends on whether we want the admin role to be irremovable. I would strongly recommend making it persist, as mentioned in the issue summary.
michaelfavia’s picture

Today was FOSS day at our office today so Cole, Mika Travis and I took this a little further.

We prevented the deletion of the last user with the administrative role in the bulk operations screen. We chose to abort the operation entirely instead of let it proceed deleting until the very last user because it seemed very arbitrary to leave one unlucky soul at the end to be chased down for access.

We did:
* We prevented the deletion of the administrative role.
* We removed some special casing preventing you from deleting UID1 in both bulk and user/#/delete.
* We prevented users from deleting last user with admin role via bulk.

What remains:
* Prevent individual last user from deleting self on user/#/delete
* Prevent removal of last administrative role in both bulk and individual form. (we took a shot at this and its also almost done. Will update monday).

Status: Needs review » Needs work

The last submitted patch, 80: 540008-uid1.patch, failed testing.
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

kristiaanvandeneynde’s picture

Any user with the currently
+ * designated "administrator" role will always pass this check.

This implies there can only be one admin role. While that might be a good idea for D9, we can't put the genie back in the bottle for D8. We have the possibility for multiple admin roles, so we should refer to that concept as such. There is no "the" admin role.

Users with this role will bypass all permission access checks.

This is not true. They don't bypass any access check, they just receive all permissions. The current text is actually more accurate. I would leave it as such.

+    $admin_roles = $this->roleStorage->getQuery()
+      ->condition('is_admin', TRUE)
+      ->execute();
+    $admin_role = reset($admin_roles);

Same comment as above. There is no one admin role to rule them all. We need to loop over all "is_admin" roles.

+        $perm_roles = array(RoleInterface::ANONYMOUS_ID, RoleInterface::AUTHENTICATED_ID, RoleInterface::ADMINISTRATOR_ID);
+        if (in_array($entity->id(), $perm_roles)) {

I'd use a more descriptive variable name ($internal_roles for instance) and we should use in_array() with the third argument set to TRUE.

michaelfavia’s picture

@kristiaanvandeneynde: Thanks for taking a look. Unless I'm mistaken in drupal 8.4.x branch hasPermission() in Role.php does indeed bypass the permission checks if the isAdmin property:

  public function hasPermission($permission) {
    if ($this->isAdmin()) {
      return TRUE;
    }
    return in_array($permission, $this->permissions);
  }

You'll notice that the permissions are not actually added to the role and are instantly removed if you undesignated it as the admin role.We learned this the hard way by locking ourselves out of the site upon redesignation of that role. :)

Regarding the multiple role selection, you can see on AccountSettingsForm->buildForm():

$form['admin_role']['user_admin_role'] = [
      '#type' => 'select',
      '#title' => $this->t('Administrator role'),
      '#empty_value' => '',
      '#default_value' => $default_value,
      '#options' => $roles,
      '#description' => $this->t('Users with this role will bypass all permission access checks. Changing this setting will not affect existing permissions.'),
      // Don't allow to select a single admin role in case multiple roles got
      // marked as admin role already.
      '#access' => count($admin_roles) <= 1,
    ];

This is currently a single select with an #access check that is disabled from changing if someone had successfully set more than one in a prior version of the form perhaps? So it looks like it is a vestigial option as well. There was special casing for handling these historical possibilities elsewhere and we probably need to allow this in our changes I agree. I'd love some explanation form anyone familiar with the evolution of this option but I can dig into annotate if nobody remembers.

Also happy to change variable names like you mentioned as its probably the hardest thing I do all day and have almost no preference :).

kristiaanvandeneynde’s picture

I said they don't bypass any access check. They do bypass permission checks. This is an important difference because we can now test access checks without having to worry about some users not being pulled through all of the functions. I'm aware of the code in hasPermission(), but the subtle difference here is key.

Re multiple admin roles: It's something we can't undo now, I guess. In D9 we can force one admin role and be done with it, but now that the cat's out of the bag already, we can't put it back in for D8. If we remove the concept of multiple admin roles, we might break someone's build. Silly as it may be to have multiple roles that return TRUE for isAdmin().

kristiaanvandeneynde’s picture

Also happy to change variable names like you mentioned as its probably the hardest thing I do all day and have almost no preference :)

If you want something harder to do, you could fix the tests you broke in your last patch :)

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
Issue tags: -Usability, -Needs screenshots
FileSize
1.18 KB
50.17 KB

Right, reverting back to the patch from #78, including the prevention of deleting the admin role (see interdiff). This is now a complete MVP (IMO) for this issue.

Whether we want to babysit users by preventing delete-last-admin actions from the UI or document a fix on drupal.org like we have in the past is still up for debate but should not affect the patch in its current form. If we choose to document, we can do that before this patch goes in. If we choose to babysit, we should consider doing that in a follow-up perhaps to avoid polluting the patch from this issue.

One patch is about fixing a drupalism, the other would be about making the fixed drupalism more mistake-proof.

Status: Needs review » Needs work

The last submitted patch, 86: drupal-540008-86.patch, failed testing. View results

kristiaanvandeneynde’s picture

Status: Needs work » Needs review

This patch broke 2 tests: DateFormatAccessControlHandlerTest and MenuAccessControlHandlerTest, which were both introduced June 12th in #2843772: EntityResource: Provide comprehensive test coverage for DateFormat entity.

This is why we need this in core ASAP. As long as we keep UID 1 being special, we will be able to write poor test cases because we make assumptions about UID 1. All tests should be explicit about the circumstances under which they should run.

kristiaanvandeneynde’s picture

FileSize
5.17 KB
55.06 KB
kristiaanvandeneynde’s picture

Retesting to see if it still applies.

kristiaanvandeneynde’s picture

johnennew’s picture

Status: Needs review » Reviewed & tested by the community

This looks good to me and addresses the fixes in the tests as described.

cilefen’s picture

It does not look as if this has had signoff from the usability maintainers.

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Session/PermissionsHashGenerator.php
    @@ -55,10 +66,14 @@ public function __construct(PrivateKey $private_key, CacheBackendInterface $cach
    -    if ($account->id() == 1) {
    -      return $this->hash('is-super-user');
    ...
    +      if ($role->isAdmin()) {
    +        return $this->hash('is-super-user');
    

    A) this looks scarily different and looks like a sec problem

    B) 'is-super-user' no longer makes sense, this should now become 'is-admin'

  2. +++ b/core/lib/Drupal/Core/Session/PermissionsHashGenerator.php
    @@ -55,10 +66,14 @@ public function __construct(PrivateKey $private_key, CacheBackendInterface $cach
    +    // Admin roles can always access all permissions. Use a different, unique
    

    "access all permissions" sounds very strange.

  3. +++ b/core/lib/Drupal/Core/Test/FunctionalTestSetupTrait.php
    index bdea71d..bed4521 100644
    --- a/core/modules/comment/tests/src/Kernel/CommentDefaultFormatterCacheTagsTest.php
    

    I don't understand why this test is being modified?

  4. +++ b/core/modules/comment/tests/src/Kernel/Views/CommentViewsKernelTestBase.php
    @@ -63,6 +63,8 @@ protected function setUp($import_test_views = TRUE) {
         $admin_role = Role::create(['id' => 'admin']);
         $admin_role->grantPermission('administer comments');
    +    $admin_role->grantPermission('access comments');
    +    $admin_role->grantPermission('post comments');
         $admin_role->save();
     
    

    This should not be necessary AFAICT.

  5. +++ b/core/modules/filter/tests/src/Kernel/TextFormatElementFormTest.php
    @@ -44,15 +44,16 @@ protected function setUp() {
         /* @var \Drupal\user\RoleInterface $role */
         $role = Role::create([
           'id' => 'admin',
           'label' => 'admin',
         ]);
    -    $role->grantPermission($filter_test_format->getPermissionName());
    +    foreach (FilterFormat::loadMultiple(['full_html', 'filtered_html', 'filter_test']) as $format) {
    +      /* @var \Drupal\filter\FilterFormatInterface $format */
    +      $role->grantPermission($format->getPermissionName());
    +    }
    

    Why does this suddenly need access to so many more formats?

  6. +++ b/core/modules/user/src/Entity/User.php
    @@ -81,11 +81,14 @@ public function preSave(EntityStorageInterface $storage) {
         // Make sure that the authenticated/anonymous roles are not persisted.
    -    foreach ($this->get('roles') as $index => $item) {
    -      if (in_array($item->target_id, [RoleInterface::ANONYMOUS_ID, RoleInterface::AUTHENTICATED_ID])) {
    -        $this->get('roles')->offsetUnset($index);
    +    $internal = [RoleInterface::ANONYMOUS_ID, RoleInterface::AUTHENTICATED_ID];
    +    $role_refs = $this->get('roles')->getValue();
    +    foreach ($role_refs as $index => $role_ref) {
    +      if (in_array($role_ref['target_id'], $internal)) {
    +        unset($role_refs[$index]);
           }
         }
    +    $this->get('roles')->setValue($role_refs);
    

    I have no idea what is changing here.

  7. +++ b/core/modules/user/src/RoleAccessControlHandler.php
    @@ -20,7 +20,12 @@ class RoleAccessControlHandler extends EntityAccessControlHandler {
    +        $internal_roles = [
    +          RoleInterface::ANONYMOUS_ID,
    +          RoleInterface::AUTHENTICATED_ID,
    +          RoleInterface::ADMINISTRATOR_ID,
    +        ];
    

    Why is this list not a constant on RoleInterface?

  8. +++ b/core/modules/user/src/Tests/UserPermissionsTest.php
    @@ -47,10 +47,6 @@ public function testUserPermissionChanges() {
    -    // Create an additional role and mark it as admin role.
    -    Role::create(['is_admin' => TRUE, 'id' => 'administrator', 'label' => 'Administrator'])->save();
    -    $storage->resetCache();
    

    This is no longer necessary because it now exists by default. That makes sense.

  9. +++ b/core/modules/user/src/Tests/UserPermissionsTest.php
    @@ -96,8 +92,8 @@ public function testAdministratorRole() {
    +    $this->assertOptionSelected('edit-user-admin-role', 'administrator', 'Administration role defaults to none.');
    

    This should use the constant.

  10. +++ b/core/modules/user/user.install
    @@ -98,3 +103,27 @@ function user_update_8100() {
    +function user_update_8101() {
    
    +++ b/core/modules/user/user.post_update.php
    @@ -20,3 +22,12 @@ function user_post_update_enforce_order_of_permissions() {
    +function user_post_update_grant_user_1_admin_role() {
    

    Great, there's an update path! But I don't see test coverage for it yet.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review

A) this looks scarily different and looks like a sec problem
B) 'is-super-user' no longer makes sense, this should now become 'is-admin'

A) It's not a security problem, it basically does what Drupal has been doing for user 1. Both user 1 and admin roles had the concept of "has all permissions", yet we were only short-circuiting hash generation for UID1. Now we are consistently doing so for all admin roles.
B) I kept it as is to not mess with existing cache entries, but I guess we should change it to remove all references to the concept of super users. Will change to is-admin.

"access all permissions" sounds very strange.

Will change to "Admin roles have all permissions implicitly assigned."

I don't understand why this test is being modified?

This should not be necessary AFAICT.

It's because of comment_install needing to run for the comment tracker to work. Before it didn't care about the amount of comments because UID1=god but now it does and it can't find any comments so it incorrectly fails. See #74

Why does this suddenly need access to so many more formats?

Because the test checked for those formats being available, which they weren't. So we need to grant the test user access to them in order for them to show up again:

    $this->assertRaw('<h4>Full HTML</h4>');
    $this->assertRaw('<h4>Filtered HTML</h4>');
    $this->assertRaw('<h4>Test format</h4>');
I have no idea what is changing here.

Some weird bug with offsets that previously didn't rear its ugly head but did when we added a third role to the logic. See #49. That said, we are now back to 2 special roles in that part of the code, so this change could be reverted. But the bug would still remain, albeit hidden once again. So perhaps leave the fix in place?

Why is this list not a constant on RoleInterface?

The roles themselves are already constants. Sometimes we need just anonymous/authenticated (User::preSave), sometimes we need anon/auth/admin (RoleAccessControlHandler). It seems a bit strange to create constant arrays that we may need multiple variations of.

This should use the constant.

Correct, will fix.

Great, there's an update path! But I don't see test coverage for it yet.

I could write one. I haven't written post update tests yet, so I'd need to find a good example.

kristiaanvandeneynde’s picture

To summarize #95, I need to:

  • Minor: Alter a string
  • Minor: Alter a line of docs
  • Minor: Use a constant instead of a string
  • Major: Add a post-update test
  • Arguably: Revert a bugfix

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

kristiaanvandeneynde’s picture

FileSize
55.08 KB
2.18 KB

Fixed the above nitpicks. Still needs a post-update test and did not revert the bugfix mentioned above.

kristiaanvandeneynde’s picture

FileSize
56.43 KB
1.6 KB

Added the update test, never written one before and can't really run it locally so curious to see whether it goes green.

Status: Needs review » Needs work

The last submitted patch, 99: drupal-540008-99.patch, failed testing. View results

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
FileSize
57.12 KB
1.93 KB

This should work.

heddn’s picture

Issue tags: +Needs change record

I wonder if this deserves mention in some type of core announcement, prior to commit? We have a ton of contrib modules, drush extensions, behat tests, etc that are going to fall flat on their face if this is committed. They would still all assume the prior special logic around user 1.

SKAUGHT’s picture

Issue tags: +DX

@change record

dare i ask: is this really going to be committed?

certainly i understand the ticket as a request and respect the work toward it...but.. i like having user 1. (: He's very much as core to drupal as Node is.

In terms of develop workflow it certainly is a major change. Disruptive, even I dare say.

tim.plunkett’s picture

timmillwood’s picture

Assigned: kristiaanvandeneynde » Unassigned

Unassigning @kristiaanvandeneynde, looks like his awesome work is done and we all now need to review.

@SKAUGHT - I hope this gets committed, it'd be an awesome change, and I don't see it being that disruptive. As long as your user 1 as the "administrator" role, nothing will change.

SKAUGHT’s picture

true: change is hard sometimes.

kristiaanvandeneynde’s picture

I'm glad to see newfound interest in this issue! Hope to get it committed in 8.5.x

@SKAUGHT: Change is hard, but sometimes it's also for the better. In this case there are several benefits, some of them rather major:

  • Security improvement: Once a site has been built or has proper roles defined, you can take away the admin role from all users. This ensures there are no accounts that put your entire website at risk should they be compromised.
  • Code stability: Just look at the amount of tests I had to fix because they relied on user 1 being special. This means those tests were actually not functioning and as a result means the code they were covering was actually not properly being covered. Removing the UID1 Drupalism will ensure our tests need to run with the right permissions defined.
  • Consistency: What good is an access layer if there is a special exception that can bypass everything? An example of this being a downside is a bunch of administrative local tasks (tabs) or actions ("+"-icon links) being put behind sensible access checks, only to have all gazillion of them clutter the UI for user 1 because he has god-mode haxx turned on.
  • One weird Drupalism fewer: We need to distinguish between Drupalisms that define what Drupal is and those that negatively characterize Drupal by needlessly increasing its learning curve. The special case of UID1 belongs to the latter category. There are very few systems that still have god-mode accounts. And for good reason (see above items). So let's destroy yet another barrier for outside devs to join our project.
  • etc.
greggles’s picture

I will add that for security auditing reasons the "uid 1 = god mode" is an item I've seen on several checklists: "Does the system have an account that has unlimited access?" A yes answer might need to be justified with business requirements or it might require compensating controls added to protect against the risks created. I've seen checklists where the existence of that account means a piece of software may not be used by the organization.

My advice these days is to simply block the uid 1 account immediately after install and remove any "admin" roles.

andypost’s picture

kristiaanvandeneynde’s picture

Issue summary: View changes

Updated the issue summary to reflect what the most recent patch is doing and what still needs to be done: documentation and CR.

harings_rob’s picture

  1. +++ b/core/modules/file/tests/src/Kernel/FileManagedUnitTestBase.php
    @@ -32,7 +32,7 @@ protected function setUp() {
         // Make sure that a user with uid 1 exists, self::createFile() relies on
    

    Not sure, but as the comment here implies uid 1 is needed, we should handle this in another way.

    Maybe it is already done, but then we could test with a user id different then 1.

  2. +++ b/core/modules/user/tests/src/Kernel/UserInstallTest.php
    @@ -49,6 +50,10 @@ public function testUserInstall() {
    +    $rids = db_query('SELECT roles_target_id FROM {user__roles} WHERE entity_id = 1')->fetchCol();
    

    I know its a test, but db_query is deprecated.

I will have a look in the codebase and do some functional test

esolitos’s picture

I just discovered this issue and I am a bit perplex: I think the proposed solution is not a good one at the end of the day.
We could/should compare Drupal's user 1 as *nix user 0 (aka root) and I don't see any plan to remove that in the nearany future. :)

In my opinion a much better solution, would be not to remove User 1, but create a non super-user during the Site installation (which will be User's user, and for example having an empty password field by default to user 1 (which would prevent the ability to login via UI).
In this way the login would be only possible using a reset-link that could be generated via drush or for example via a form.

In this way the less experienced users would not gain access easily to this user and they would not be able to share it, intermediate users would need to go trough a few steps to gain access so it would be clear that it's a "special user", and yet experienced users will retain this old behaviour which many "rely" on.

Also I have seen quite a few contrib modules doing if($user->uid == 1), so removing it from core would just solve the "issue" partially.

m2c

efpapado’s picture

Status: Needs review » Needs work

I disagree with the proposed solution.
The user 1 is supposed to be used by people who know what they're doing. For all other cases, there is the role delegation.

There could be other solutions, like:

  • Hard-coding some config on settings.php that will force disable the user 1 and/or the admin role.
  • Interfering to the login callback so that user 1 cannot login through UI, only by a single sign-in link.
kristiaanvandeneynde’s picture

Status: Needs work » Needs review

@esolitos (#112)

We could/should compare Drupal's user 1 as *nix user 0.

We definitely should not! Comparing a user that needs to be there because a system could otherwise become inaccessible to a user that should not be there because we can always make the system accessible again using several tools (drush, console, database, ...) is comparing apples to oranges. The root user on *nix is a necessary evil, the UID 1 privileges in Drupal is a needless security risk.

In my opinion a much better solution, would be not to remove User 1, but create a non super-user during the Site installation (which will be User's user, and for example having an empty password field by default to user 1 (which would prevent the ability to login via UI).
In this way the login would be only possible using a reset-link that could be generated via drush or for example via a form.

We'd be having a god-mode user then who no-one can use except those with extra tools. As explained above: once you have those tools you can turn any user into a god-mode user, but only when needed. So why keep user 1?

yet experienced users will retain this old behaviour which many "rely" on.

We should not keep broken features because it would otherwise force people out of their comfort zone. If we did, we wouldn't be using OO and Symfony now.

Also I have seen quite a few contrib modules doing if($user->uid == 1)

Shame on them, really. An outsider looking at any $uid === 1 check will rightfully shiver when they find out it's a magic all-access pass. We should always use the API for access checks. If those modules had properly checked for access, which would have returned TRUE for user 1 all the same, then they wouldn't suffer from this change.

@efpapado (#113)

Hard-coding some config on settings.php that will force disable the user 1 and/or the admin role.

Putting a security risk behind a flag doesn't make it less of a security risk.

Interfering to the login callback so that user 1 cannot login through UI, only by a single sign-in link.

See reply to 2nd quote from the top.

Also, the patch in this issue is ready to go in unless it breaks some recent tests or has flawed code. So the correct status is still Needs review. Please don't change it to "Needs work" unless it actually needs work. You can disagree just fine while the patch is still under review and then we can alter the patch should there be a consensus.

AFAICT this issue needs a change record, a documentation page and people stepping out of their comfort zone. Code-wise it looks just about ready to me. Even though I'm biased for writing the patch, obviously.

kristiaanvandeneynde’s picture

Maybe I need to clarify this in a separate comment:

This will not change any new or existing sites unless you choose so.

UID 1 will still have an all-access pass, but not because of them being user 1. Instead it will be because they have the all-access admin role, which makes way more sense. They will now have to go through all access checks just like any other user. And most importantly, they can now be stripped from their god mode haxx when the situation or best practices call for it.

efpapado’s picture

Hard-coding some config on settings.php that will force disable the user 1 and/or the admin role.

Putting a security risk behind a flag doesn't make it less of a security risk.

No, but it makes it harder to exploit the permissions accidentally, if this is what it's all about.

A module could very easily implement some code that will bypass any restrictions. Then it's again "some extra tools that provide god-mode".

One last argument from my side, and I will not continue participating in this discussion.
This is a very fundamental change to the Drupal functionality and AX/DX*. I believe (maybe I'm wrong, but that's my opinion) that this type of changes should not be applied so easily, after only a short discussion between 5-10 people.

*A=Admin, D=Developer

eelkeblok’s picture

No, but it makes it harder to exploit the permissions accidentally, if this is what it's all about.

It's not what it's all about. It is also about simplifying code, AFAICT. Removing special cases reduces WTFs per minute and potential number of bugs, because less code.

kristiaanvandeneynde’s picture

I feel strongly opposed to keeping any of the UID1 Drupalism in. Either we rip it out altogether or don't touch it whatsoever and be considered a CMS with a built-in security risk for years to come.

Putting it away in a dark corner where no-one will find it is arguably worse. Because years from now someone who doesn't know about what UID1 used to mean might go: "Oh, what's this? This seems awfully useful" and still expose their website to a needless security risk. It would also mean having to keep supporting broken access checks such as $uid === 1, meaning we will never have a fully reliable access API.

One last argument from my side, and I will not continue participating in this discussion.
This is a very fundamental change to the Drupal functionality and AX/DX*. I believe (maybe I'm wrong, but that's my opinion) that this type of changes should not be applied so easily, after only a short discussion between 5-10 people.

This issue has been around for 8 years, I only 'recently' started working on a functional patch for D8. And by all means keep participating if you feel like it. Just because we disagree with one another doesn't mean your opinion is less valid. I might disagree with 9 out of 10 things you say and be baffled by the 10th argument.

tacituseu’s picture

Great work, 100% for it, magic numbers in code are never a good thing, especially those bypassing access checks.

michaelfavia’s picture

I feel strongly opposed to keeping any of the UID1 Drupalism in.

@kristiaanvandeneynde: While I don't speak for anyone but myself and my team, I think you have near universal support and agreement in this from anyone who has taken a strong look at the situation. Tying permissions to roles and assigning additive roles to users to allow multiple "root" accounts without sharing passwords is a very common pattern and one we should adopt. Attempting to work on any significantly complex project that requires collaboration between multiple agencies or stakeholders drives the point home very well if nothing else. Thanks for carrying the torch as far as you have.

malcomio’s picture

Seems like a sensible change to me - as mentioned a few times above, it'll need clear communication, but we should be encouraging contrib and custom modules to use proper access checks based on permissions.

kristiaanvandeneynde’s picture

Issue tags: -Needs change record

Here's the change record draft. We still need the documentation on how to restore the admin role using drush, drupal console and a database query and link to that documentation from the change record.

https://www.drupal.org/node/2910500

ndf’s picture

Regarding the uid == good (god?)
esolitos #112
efpapado #113
michaelfavia #120

The current situation makes development harder, because only as user 1, you can expect that (as a site builder) the website behaves as expected.
I strongly agree with kristiaanvandeneynde and others reasoning above.

Timing
When this patch gets in, it will be disruptive though. Timing and communication is therefor critical.

Something that we should discuss is if this patch must be committed early or late in the development cycle.

In this case I think early is better (today in 8.x-5.x or in 6 months early in 8.x-6.x) so that contrib maintainer can catch up.
Two other alternatives (somewhere in the middle, or as late as possible) could fool contrib maintainers and custom module developers.

We can put this in Drupal 8
I don't think we have to wait for Drupal 9. Only newish websites that are in 'build' modus are affected. Most 'in production' websites will have multiple non-user-1 accounts and those are not affected at all.

ndf’s picture

Regarding the change record

Module developers
Can we add a code-snippet for module-developers how they must replace their $uid === 1 expressions?

Should we add some example searches/greps how to find those.
($uid === 1, $uid == 1, stuff like that)?

Site builders / upgrade path
What are implications / upgrade paths when a website chose a specific name for the admin-role that is different or equal to the new admin-role? (admin, administrator, thewebsite_admin)
Should that website change something or is this always without side-effects?

Side effects?
If there are side-effects; should we consider to split the commit in a first one that makes the conversion possible and a final one that removes the $uid === 1 behaviour? Or is the conversion (of contrib and custom modules) already possible without this commit?

daften’s picture

Don't forget the impact on e.g. drush uli which logs the super-user in when no arguments are given.

kristiaanvandeneynde’s picture

Should that website change something or is this always without side-effects?

No side-effects really. The patch checks if the role exists already. If so, it turns it into an admin role. If not, it creates it.

Or is the conversion (of contrib and custom modules) already possible without this commit?

Already possible. We're talking about broken code that can already be fixed by properly calling the access layer. Tests need to run with any user other than UID 1 in order to be fixed now. Once this patch lands, they don't need to worry about their tests going green by accident because they happened to have received UID1 as their test user.

kristiaanvandeneynde’s picture

Re-testing to see whether any new tests break. The EntityResource work might break, but those tests should have been written with this patch in mind, meaning it all boils down to deleting some UID1 code.

Status: Needs review » Needs work

The last submitted patch, 101: drupal-540008-101.patch, failed testing. View results

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
FileSize
57.11 KB

Straight up re-roll using an automated three-way merge, no changes.

Status: Needs review » Needs work

The last submitted patch, 129: drupal-540008-129.patch, failed testing. View results

kristiaanvandeneynde’s picture

Status: Needs work » Needs review
FileSize
63.34 KB
6.49 KB

Well this goes to show why we need this in core ASAP. A comment test was leaking state but went green anyway because UID 1 = god and they can see all the things all the time. I fixed the state leak and it works fine now.

webchick’s picture

Issue tags: -Needs usability review

Discussed this patch in today's UX meeting.

Because of the way that this patch is written, there won't be any changes to the existing admin role UX to be concerned about here, so from that POV we are good.

The two concerns that were brought up were around:

1) What happens if a user removes the Administrator role from all users, thus removing their ability to re-add it? It's covered above the idea of a Drush/Console command to get out of this situation, and a documentation page with a vanilla SQL query. That seemed sufficient to the group (particularly the documentation page that someone can find in Google that mentions the various options). We briefly talked about the idea of in-UI validation that prevents the change, such as what we do around removing the System content block, but we do it there because a) it's really easy to do since that block is not specially marked in any way, and b) you are completely screwed if this happens because you remove the output of content from all of your pages. In this case, the only reason you'd remove the Administrator role from all users is because you want to explicitly lock down your site. You're much more unlikely to ever tamper with it otherwise.

2) Emilie also brought up what happens if a site already has a role with the machine name of 'administrator' but with a reduced set of permissions? (e.g. they actually mean "content administrator" or "spam administrator" or something) We would not want to accidentally expand peoples' permissions as part of the upgrade; this would be really easy to miss. Suggestion was that during the upgrade if we encounter a site that already has an 'administrator' role, and it isn't specially marked as an admin role/ doesn't have all checkboxes checked / whatever, then migrate the existing role to "administrator_legacy" before adding the new one and assigning it to user 1. This way there's no behaviour change.

There was another concern brought up around what happens with config managed in Features which I didn't quite grok, but it's not a UX concern, so will leave that up to the folks who brought that up. Removing the tag.

kristiaanvandeneynde’s picture

Thanks webchick and the rest of the UX team!

I'll address concern #2 ASAP in the update hook and test coverage.

kristiaanvandeneynde’s picture

FileSize
66.14 KB
6.71 KB

Let's try this one for size. Should take care of the concern outlined in #132.2

kristiaanvandeneynde’s picture

Oh wow, green in one go!

I've updated the change request to link to a documentation page I just added. See: https://www.drupal.org/node/2910500 It did mention something about migrating documentation to the new section, but I'm not sure where I'd need to put it then.

That said: The patch goes green and the UX concerns have been dealt with. Anyone willing to review and set to RTBC if satisfied?

zaporylie’s picture

What if administrator_legacy role already exists? I believe we should be more dynamic about legacy role name.

kristiaanvandeneynde’s picture

What if administrator_legacy role already exists? I believe we should be more dynamic about legacy role name.

I actually raised that during the UX meeting and webchick mentioned the odds of that happening are so slim we shouldn't account for it.

It's basically: <odds of having a non-admin administrator role> * <odds of having an administrator_legacy role>

zaporylie’s picture

Sure - it's all about odds - but since we can mitigate risk of having role name conflict can't we just cover that scenario as well? Or, if we really don't want write that piece of code, can we at least check if _legacy role already exists and throw nice update exception with information what actually happened?

By - * we * - I mean - * I can help * :)

SKAUGHT’s picture

*admin* clearly is a common base in many projects..perhaps a name other than some with *admin* at all. Naming the role "rooted" seems appropriate and less likely to be used by an exiting project.

kristiaanvandeneynde’s picture

I don't know. Normally I write code that accounts for all possible scenarios, but in this case it seems the chances are slim.

We even discussed naming the role magical_sparkling_pony to make sure no-one has it, but then there's the same discussion: "What if someone does happen to have a magical sparkling pony role?"

I'd also argue to use an incremental suffix, e.g.: administrator_legacy_3, but then the post-update hook would have to guess which role it needs to check for. We could try storing the role name in temporary storage such as state, but if that gets flushed between the update and post-update hook we'd have nothing to go on.

eelkeblok’s picture

Maybe just do a check if the backup exists and if it does, bail out with instructions on how to configure a unique name in the settings.php.

kristiaanvandeneynde’s picture

That could work.

zaporylie’s picture

I'm ok with suggestion in #141. Edge-case instructions could be provided in change record ("In unlikely event of a name collision...").

kristiaanvandeneynde’s picture

FileSize
66.73 KB
7.48 KB

Okay I've taken care of the concern in #141. If the patch goes green I'll update the CR.

P.S.: I also updated the test to meet coding standards and hard-coded the 'administrator' and 'administrator_legacy' role IDs because update hooks should hard-code such values instead of using variables that might change over time.

Edit: Instead of bailing out and then providing instructions, I simply opted for a configuration setting. You're supposed to read update notes when upgrading from one major version to another, no?

kristiaanvandeneynde’s picture

Updated the change record. We good to go RTBC now?

Anonymous’s picture

Many thanks to @kristiaanvandeneynde and all not indifferent here! This issue is super! ⭐⭐⭐

Honestly, I'm not a big expert, and I did not even know about the 'user 1' problem until I saw it with this issue. But now I'm very glad that it's going to fix state. Just look at these changes:

+++ b/core/lib/Drupal/Core/Cache/Context/UserRolesCacheContext.php
@@ -27,13 +27,6 @@ public static function getLabel() {
   public function getContext($role = NULL) {
...
-    if ($this->user->id() == 1) {
-      return 'is-super-user';
-    }

+++ b/core/lib/Drupal/Core/Session/UserSession.php
@@ -103,11 +103,6 @@ public function getRoles($exclude_locked_roles = FALSE) {
   public function hasPermission($permission) {
-    // User #1 has all privileges.
-    if ((int) $this->id() === 1) {
-      return TRUE;
-    }
-
     return $this->getRoleStorage()->isPermissionInRoles($permission, $this->getRoles());
   }

+++ b/core/modules/views/tests/src/Kernel/Plugin/RowRenderCacheTest.php
@@ -188,11 +188,10 @@ protected function doTestRenderedOutput(AccountInterface $account, $check_cache
-        $user_context = !$account->hasPermission('edit any test content') ? 'user' : 'user.permissions';
...
-            'contexts' => ['languages:language_interface', 'theme', $user_context],
+            'contexts' => ['languages:language_interface', 'theme', 'user.permissions'],

+++ b/core/profiles/standard/standard.install
@@ -5,7 +5,6 @@
-use Drupal\user\Entity\User;
...
@@ -28,11 +27,6 @@ function standard_install() {

@@ -28,11 +27,6 @@ function standard_install() {
-  // Assign user 1 the "administrator" role.
-  $user = User::load(1);
-  $user->roles[] = 'administrator';
-  $user->save();

They are wonderful!

+++ b/core/modules/user/src/AccountSettingsForm.php
@@ -104,8 +104,8 @@ public function buildForm(array $form, FormStateInterface $form_state) {
-    // Do not allow users to set the anonymous or authenticated user roles as the
-    // administrator role.
+    // Do not allow users to set the anonymous or authenticated user roles as
+    // the administrator role.

Hah! Achievement for opening this hiding place 🥇

'GOD' regim with uid 1 definitely a problem for the correct account settings. And, of course, still admire how this fix helps to identify a series of unluck tests! Great works again!


Going to post two additional review (one with major points, and one with minor points on #144 patch).
Anonymous’s picture

3 major points:

1.
As already mentioned, this issue perfectly reveals a number of problems with our tests. It would be great to solve them separately. The current patch will become easier, incorrect tests will be quickly fixed (and possibly further improved). I'll be glad to open follow-up issues.

For test-only and prevent regression, we can just add unused user, like this:

test() {
...
+ // @todo: Remove $_user1 when https://www.drupal.org/node/540008.
+ $_user1 = User::create([]);
   $user = User::create([
...

2.
Looks like IsSuperUserCacheContext class still relies on the user1.

3.

+++ b/core/lib/Drupal/Core/Session/PermissionsHashGenerator.php
@@ -55,10 +66,14 @@ public function __construct(PrivateKey $private_key, CacheBackendInterface $cach
   public function generate(AccountInterface $account) {
-    // User 1 is the super user, and can always access all permissions. Use a
-    // different, unique identifier for the hash.
-    if ($account->id() == 1) {
-      return $this->hash('is-super-user');
+    // Admin roles have all permissions implicitly assigned. Use a different,
+    // unique identifier for the hash.
+    $storage = $this->entityTypeManager->getStorage('user_role');
+    foreach ($storage->loadMultiple($account->getRoles()) as $role) {
+      /** @var \Drupal\user\RoleInterface $role */
+      if ($role->isAdmin()) {
+        return $this->hash('is-admin');
+      }
     }

Why we need save this behavior? In other places of patch, such places are simply deleted. Do not harm the same hash key when there are several admin users?

Anonymous’s picture

Minor points (i will glad to help with fix all of them):

  1. +++ b/core/modules/comment/tests/src/Kernel/CommentDefaultFormatterCacheTagsTest.php
    @@ -48,7 +50,7 @@ protected function setUp() {
         // Set the current user to one that can access comments. Specifically, this
         // user does not have access to the 'administer comments' permission, to
    ...
    -    $current_user->setAccount($this->createUser([], ['access comments']));
    +    $current_user->setAccount($this->createUser([], ['access comments', 'post comments']));
    

    Nit:

    -     // Set the current user to one that can access comments...
    +     //  Set the current user to one that can access and post comments...
  2. +++ b/core/modules/filter/tests/src/Kernel/TextFormatElementFormTest.php
    @@ -44,15 +44,16 @@ protected function setUp() {
    -    $role->grantPermission($filter_test_format->getPermissionName());
    +    foreach (FilterFormat::loadMultiple(['full_html', 'filtered_html', 'filter_test']) as $format) {
    

    It may be better just to check that other formats are inaccessible, instead of providing access to them:

    -    $this->assertNoRaw('<h4>Full HTML</h4>');
    -    $this->assertNoRaw('<h4>Filtered HTML</h4>');
    +    $this->assertNoRaw('<h4>Full HTML</h4>');
    +    $this->assertNoRaw('<h4>Filtered HTML</h4>');
        $this->assertRaw('<h4>Test format</h4>');

    This is just one of those moments that it is great to solve in a separate issue with the participation of the mantainer (first point in #148)

  3. +++ b/core/modules/system/tests/src/Kernel/DateFormatAccessControlHandlerTest.php
    @@ -53,18 +53,10 @@ protected function setUp() {
       public function testAccess($which_user, $which_entity, $view_label_access_result, $view_access_result, $update_access_result, $delete_access_result, $create_access_result) {
    ...
    +    $permissions = ($which_user === 'admin')
    +      ? ['administer site configuration']
    +      : [];
    +    $user = $this->drupalCreateUser($permissions);
    

    Can we immediately set permissions in the data provider? Like:

    'permissionless + locked' => [
    -        'permissionless',
    +        []',
    ...
    'admin + unlocked' => [
    -        'admin',
    +        ['administer site configuration']
  4. +++ b/core/modules/user/src/Entity/User.php
    @@ -82,11 +82,14 @@ public function preSave(EntityStorageInterface $storage) {
    -    foreach ($this->get('roles') as $index => $item) {
    -      if (in_array($item->target_id, [RoleInterface::ANONYMOUS_ID, RoleInterface::AUTHENTICATED_ID])) {
    -        $this->get('roles')->offsetUnset($index);
    +    $internal = [RoleInterface::ANONYMOUS_ID, RoleInterface::AUTHENTICATED_ID];
    +    $role_refs = $this->get('roles')->getValue();
    +    foreach ($role_refs as $index => $role_ref) {
    +      if (in_array($role_ref['target_id'], $internal)) {
    +        unset($role_refs[$index]);
           }
         }
    

    #49: very useful information, can add as comment to patch?
    Also maybe nit renaming $internal/$locked. Because ADMINISTRATOR_ID also is internal, but not in this list.

  5. +++ b/core/modules/user/src/Tests/Update/UserUpdateUserOneAdminTest.php
    @@ -0,0 +1,106 @@
    +class UserUpdateUserOneAdminTest extends UpdatePathTestBase {
    

    It should be phpunit.

  6. +++ b/core/modules/user/src/Tests/UserPermissionsTest.php
    @@ -96,8 +92,8 @@ public function testAdministratorRole() {
    -    $this->assertOptionSelected('edit-user-admin-role', '', 'Administration role defaults to none.');
    ...
    +    $this->assertOptionSelected('edit-user-admin-role', RoleInterface::ADMINISTRATOR_ID, 'Administration role defaults to none.');
    

    nit rudiment in assert message:

    - 'Administration role defaults to none.'
    + 'Administration role defaults to "administration".'
    
  7. +++ b/core/modules/views/tests/src/Kernel/Handler/FieldFieldAccessTestBase.php
    @@ -42,7 +42,7 @@ protected function setUp($import_test_views = TRUE) {
    -      'permissions' => ['view test entity field'],
    +      'permissions' => ['view test entity field', 'access content'],
    

    There's no problem here, just wanted to be happy again that it's great. Once upon a time, I wanted to remove the 'view test entity field' while working on another issue. But @Wim Leers prevented me. Now I understand why the test worked without it. Fix user1 is really very useful step!

  8. +++ b/core/modules/views/tests/src/Kernel/Handler/FieldFieldTest.php
    @@ -74,7 +74,7 @@ protected function setUp($import_test_views = TRUE) {
    +    $this->adminUser = User::create(['name' => $this->randomString(), 'roles' => ['administrator']]);
    

    This should use the constant like #94.9.

  9. +++ b/core/tests/Drupal/KernelTests/Core/Render/RenderCacheTest.php
    @@ -32,63 +32,52 @@ protected function setUp() {
    -  public function testUser1PermissionContext() {
    +  public function testPermissionContext() {
    

    Maybe save 'User' in name of function, like :

    -  public function testUser1PermissionContext() {
    +  public function testUserPermissionContext() {
  10. #111: seems without feedback.
  11. Perhaps after the commit, it is worth open issue for clean-up for comments like user 1 (59 matches now), The "#1" admin user with replace on the 'admin'. Not now for avoid unnecessary noise
zaporylie’s picture

Anonymous’s picture

@zaporylie, thank you! Can you made interdiff with diff --find-copies (or -M), for a more readable comparison of differences after convert to phpunit?

zaporylie’s picture

FileSize
1.18 KB

true...

kristiaanvandeneynde’s picture

Thanks for the thorough review vaplas, below are my answers.

147.1: No, this patch will break several tests because of the way they were written. Not fixing them properly here and instead offloading that to separate issues is a bad idea. We can't add something to core and then have every patch in the issue queue go red until we commit all of the separate test fixes.

147.2 Good catch! We can't remove that one though because it would be a backwards compatibility break. We could open up a separate issue for that one after this patch lands, asking for it to be marked as deprecated and making it return 0 all the time.

147.3: See #94 and other mentions of is-admin in this issue.

148.1: Why would you add 2 spaces there? Coding standards dictate only one leading space for a comment.

148.2: This patch fixes that test's permissions only. If you want to refactor it to make more sense, that should indeed happen in a separate issue.

148.3: Perhaps that could be moved to the data provider, yes. But same answer as 184.2: Separate issue :)

148.4: Even though #49 explains why the old code was broken, the new code is self-explanatory. Adding a comment saying why we can't use offsetUnset() would only confuse people reading the code that is actually there.

148.5: Yup, it should be. I couldn't find any tests for post-update hooks using PhpUnit so I went with the one that is used elsewhere by core. Thanks vaplas and zaporylie for taking care of that one.

148.6: Good catch, this needs fixing (patch attached).

148.7: Nothing to say here.

148.8: Good catch, this could be cleaned up (patch attached).

148.9: The cache context is based on user permissions, yes. But the full name should be user.roles.permissions (there's an issue about this). So then we'd end up with a function like testUserRolesPermissionContext. I'd rather keep it short, self-explanatory and independent of the cache context's internal name. That said, I don't care about this one that much. If people want to change it, that's fine.

148.10: The FileManagedUnitTestBase test could use some refactoring in createFile() to use something like $this->user->id() but other than that, its only vice is hard-coding a user ID. So that test doesn't need to be fixed here but in a separate issue. Using a db_query() in a test is the least of my concern right now. It's readable and easily picked up by a huge find-and-replace ticket when we're closer to 9.0.x. Feel free to fix it if you're really bothered by it, though.

148.11: Yup, completely agree. Some text clean-up will be needed but that's non-functional so can be done in a follow-up.

Anonymous’s picture

Thanks for full answer @kristiaanvandeneynde. And sorry for delay with feedback:

147.1:
Yep. But we could:

  1. Сreate a parallel [meta] issue about the implicit use of user1 in the tests.
  2. Then open the issues for each problem test.
  3. After fixing all of them, somehow temp prevent the use user1 in test (example, via create two user in User::create, if the user/1 does not exist, or pre-creating the first user).

We do not need to remove any parts from this patch. Simply, after one of the associated issues is commited, remove part of it from here.
Pros:

  • Quick (i hope) fix problems with the check of permissions, which is now in our core tests.
  • Popularize the "user 1" problem.
  • Increase the synchronization between branches (if for some reason this issue is not comitted to both branches)
  • Simplify the study of the patch, because its size and area of change will decrease with each commit

Cons:

  • We need to expend energy on worries about this.

Of course, if someone from committers to say, that the fuss with the partition is not needed, I'll be glad!)

147.2: ok
147.3: I found only #94 and #95 with 'is-admin' word. I just wanted to draw attention to the fact that the:
before: 1 hash <-> 1 user (with uid 1),
after: 1 hash <-* many users (with admin roles)
But because isAdmin() always means all permissions. And saving the 'special hash' is necessary for BC (maybe, performance ). Ok. Sorry for noise.

148.1: Hah) looks really like this, but i want to point on

- access
+ access and post

This nit fix of comments after changed permissions:

-    $current_user->setAccount($this->createUser([], ['access comments']));
+    $current_user->setAccount($this->createUser([], ['access comments', 'post comments']));

148.2-11: Thank you for the explanations. Ok for me.

Added few additional issues about 'user 1' problem. TL;DR for me: without the user1 is better :)

Anonymous’s picture

One more question. Now we can not delete the 'administrator' role, because

+++ b/core/modules/user/src/RoleAccessControlHandler.php
@@ -20,7 +20,12 @@ class RoleAccessControlHandler extends EntityAccessControlHandler {
       case 'delete':
...
+        $internal_roles = [
+          RoleInterface::ANONYMOUS_ID,
+          RoleInterface::AUTHENTICATED_ID,
+          RoleInterface::ADMINISTRATOR_ID,
+        ];
+        if (in_array($entity->id(), $internal_roles)) {

But we can change is-admin flag for this role:

class Role extends ConfigEntityBase implements RoleInterface {
...
  public function setIsAdmin($is_admin) {
    $this->is_admin = $is_admin;
    return $this;
  }

There may be a potential problem, when on site this role is turned into non-admin (it is not deleted, because it's impossible) and in some place of code will be use ADMINISTRATOR_ID (instead of isAdmin()).

It may be worthwhile to prevent the delete by isAdmin(), or prevent set is_admin flag to FALSE for ADMINISTRATOR_ID?

kristiaanvandeneynde’s picture

Re 153 re 147.1: I really don't see the benefit and think it will only add more red tape, preventing this issue from getting committed before 8.5.x so I'm sorry but I'm not going to be spending any time on that idea.

Re #154: Good catch, I'll attach a patch that does exactly this and removes the part of the UI that allows you to choose your administrator role at admin/config/people/accounts because that UI no longer makes sense.

kristiaanvandeneynde’s picture

FileSize
70.41 KB
5.15 KB

Okay so attached is a patch that removes the UI where you can choose which role is the Admin role. I did not put extra checks behind the use of the API methods isAdmin() and setIsAdmin() because of the fact that we also do not prevent anyone from deleting the Anonymous, Authenticated or Administrator role through code.

The code in RoleAccessControlHandler prevents you from deleting said roles through the Drupal admin UI, but you can still break your system by calling Role::load('anonymous')->delete(); so it would not make sense to babysit people regarding the isAdmin() flag when we don't do the same for the deletion of important roles. If you want to see this changed (in both cases), a follow-up issue may be the right place to do so.

I'll update the CR to list the UI change.

Anonymous’s picture

Status: Needs review » Reviewed & tested by the community

Thanks @kristiaanvandeneynde. Super!
Loss of flexibility in choosing an admin role seems like an excellent solution! The access rules must be clear and simple as a stick.

Of course, as the Challenge, you can think of a case where such switching will be useful. Example:

On the site there are two groups of people, between which you need to switch administrator permissions from time to time.

But this is an extremely specific case, which is easily implemented through the API:

Role::load('admin1')->setIsAdmin(FALSE)->save();
Role::load('admin2')->setIsAdmin(TRUE)->save();

It is more important not to create excessive noise in the settings, which can cause loss of control over the site.

Now we have hold 'Administrator' role by default, without the possibility to spoil it through the UI (which is available to untrained users), but with the easy possibility of changing through the API.

So, this issue gives the mass of profit:

  • preventing uncontrolled (and unclear) permissions of user/1.
  • fixing, preventing future occurrences, and preventing regression in test permission assertions.
  • terminationing misconduct in all contrib/custom code, where the permission logic does not include 'god mode' for the user/1.
  • allows you to remove the hacker's sight from user/1

And backward compatibility is saved, since user/1 continues to have the role of administrator.
And super-careful update (with support legacy transfer of exist role with same id, and simply configuration of name in rare case of double collisions).

It seems that the questions of other participants were also answered. Therefore, I pick up the status to RTBC, because the further movement depends on commiters.

Thanks for amazing works again!

kristiaanvandeneynde’s picture

Status: Reviewed & tested by the community » Needs review

Cool, thanks for the very thorough reviews vaplas!

kristiaanvandeneynde’s picture

Status: Needs review » Reviewed & tested by the community

Lol, my bad.

kristiaanvandeneynde’s picture

Out of curiosity: Who is able to commit this?

Because it's such a big patch I could understand them being a bit hesitant to commit it. So if they'd like to go over the thing as a whole, I'm willing to jump on a call and talk them through it.

Wim Leers’s picture

Shouldn't this also deprecate \Drupal\Core\Cache\Context\IsSuperUserCacheContext and mark it for removal in Drupal 9?

kristiaanvandeneynde’s picture

Will do. I thought I'd do it in a follow-up as per #152:

147.2 Good catch! We can't remove that one though because it would be a backwards compatibility break. We could open up a separate issue for that one after this patch lands, asking for it to be marked as deprecated and making it return 0 all the time.

I've marked it as deprecated and made it return 0 all the time as there is now no longer a "superuser".

catch’s picture

Please move the test fixes into one spin-off issue that we can commit before this one. That will mean two smaller patches to review and will likely result in this issue getting committed sooner rather than later. I'm still catching up with the past couple of dozen comments or so and haven't reviewed the patch recently.

catch’s picture

Status: Reviewed & tested by the community » Needs work
kristiaanvandeneynde’s picture

Before I go ahead and do that: What do we do about new tests that break this patch?

Suppose I go through the effort of splitting this patch up into two parts and the test-fixes-only part gets committed. Then another few weeks pass before someone looks at this issue and in the mean time even more "broken" tests were added to core. That would mean this issue's patch would again go red until we fix those tests. At that point we'd be back to square one: Splitting off tests to make this issue readable, chasing HEAD for god knows how long.

Also: What about the gray area fixes? Tests that were broken but after the UID 1 change made no sense any more or needed drastic change. Do those remain in this issue's patch or go into the spin-off issue's patch as they may make that patch go red without the code from this patch.

Or shall I start by splitting off only those tests that have clear fixes to obvious problems?

catch’s picture

Suppose I go through the effort of splitting this patch up into two parts and the test-fixes-only part gets committed. Then another few weeks pass before someone looks at this issue and in the mean time even more "broken" tests were added to core. That would mean this issue's patch would again go red until we fix those tests. At that point we'd be back to square one: Splitting off tests to make this issue readable, chasing HEAD for god knows how long.

That can happen while this issue is being reviewed too, I don't see the difference. And it wouldn't be square one, because one or two new test failures doesn't suddenly reintroduce the already fixed ones.

Tests that were broken but after the UID 1 change made no sense any more or needed drastic change. Do those remain in this issue's patch or go into the spin-off issue's patch as they may make that patch go red without the code from this patch.

If there are test changes that are actually required for the patch it's fine to leave those in, that's a good reason to split the patch as well, since it makes it easier to review those changes vs. the fixes to tests with bad assumptions.

kristiaanvandeneynde’s picture

Cool, I'll start by splitting off the obvious ones then.

kristiaanvandeneynde’s picture

Title: Remove the special behavior of uid #1. » [PP-1] Remove the special behavior of uid #1.
Status: Needs work » Needs review
FileSize
52.21 KB

Okay so renaming this issue to reflect that it needs a different patch first: #2917584: Some tests only go green because they happen to run as UID1 Managed to split off quite a few tests, although this patch remains pretty big. Not running tests because they'd go red anyway.

Reasons why the remaining tests are still here (bold for edge case):

  • FunctionalTestSetupTrait: Test changed to reflect new behavior, i.e.: test wasn't broken
  • FilterDefaultConfigTest: Test changed to reflect new behavior, i.e.: test wasn't broken
  • RoleResourceTestBase: Test changed to reflect new behavior, i.e.: test wasn't broken
  • UninstallTest: Test changed to reflect new behavior, i.e.: test wasn't broken
  • DateFormatAccessControlHandlerTest: This test was written with this issue in mind (thanks Wim Leers), this patch removes the UID 1 special casing
  • MenuAccessControlHandlerTest: This test was written with this issue in mind (thanks Wim Leers), this patch removes the UID 1 special casing
  • UserPermissionsTest: Test changed to reflect new behavior, i.e.: test wasn't broken
  • UserUpdateUserOneAdminTest: This test was introduced by this patch
  • UserEntityTest: Test changed to reflect new behavior, i.e.: test wasn't broken
  • UserInstallTest: Test changed to reflect new behavior, i.e.: test wasn't broken
  • FieldFieldTest: This test was broken, but the fix is much cleaner here. See: #2917584-4: Some tests only go green because they happen to run as UID1
  • RowRenderCacheText: This test was broken, but the fix is much cleaner here. See: #2917584-4: Some tests only go green because they happen to run as UID1. Also, see comment #70
  • RenderCacheTest: This test was explicitly testing for UID1. That has been removed to reflect the new behavior.
  • PermissionsHashGeneratorTest: Test changed to reflect new behavior, i.e.: test wasn't broken
kristiaanvandeneynde’s picture

I had to re-add the FileManagedUnitTestBase change here because we could not fix it separately in #2917584: Some tests only go green because they happen to run as UID1 due to it being a base class and thus we would be at risk of breaking BC. See #2917584-23: Some tests only go green because they happen to run as UID1 for more info (last paragraph of that comment).

So I've added it back into this issue's patch. Still do not test as this patch requires the other one to go in first.

David_Rothstein’s picture

As much as I like the premise behind this issue, I am concerned about the backwards compatibility implications of doing this in a stable release. As far as I can tell, committing this patch would basically mean that code which previously checked $user->id() == 1 in order to grant access to something would suddenly lead to an access bypass security issue on sites which take advantage of this new feature.

Anonymous’s picture

#170: If I correctly understood the question, then @kristiaanvandeneynde answered it several times, example #115.

kristiaanvandeneynde’s picture

@David_Rothstein If anyone is writing code with $user->id() == 1 in it, then that code is the security issue. We've been training people for years now to use permission checks instead of role or UID checks. I've answered this quite a few times in this issue already: we're not breaking backwards compatibility in any way; we're merely exposing poorly written code.

The sooner this goes in before 8.5.0, the more time we'll have to start raising awareness. Suppose this lands by the end of the year, that will still give people 3 months to scan their code for god-mode checks and replace them with proper permission checks.

I really don't think we should count exposing bad code as a BC break. There are no API changes in this patch, only behavioral changes. And very bad behavior at that if I might say so myself :)

heddn’s picture

I think there's a point in #170. Many contrib modules followed what core has done for years and checked on user == 1. I even see a lot of custom code littered through many sites that do the same thing. The second this patch lands, we now have the potential for access bypass. Its unfortunate, isn't it. That by making things more secure, we run the risk of making things less secure. Can we get a coder rule written that checks for this type of behavior? It won't catch everything, but it might catch some. Something like $user->id == 1 || $user->id() == 1 is bad form.

I also see behat tests and drupal behat that assume this too. I'm asking us to brainstorm the worst case potential here and see if we can head any of it off.

eelkeblok’s picture

I wonder how big that problem is; I expect most modules - especially those written for D8 - will simply do access checks through the regular APIs. I have encountered code that specifically checks for uid = 1, but that was exactly because core treats it as special and thus it can't be treated as "just another user".

Either way, for any sites that are upgraded to 8.5.0, uid = 1 will get the admin role assigned, so functionally, there shouldn't be any change. Even if something goes wrong, uid = 1 being special is deeply ingrained in any workflow around any existing Drupal website, so if the user does not get the role assigned, it will be noticed rather quickly (and it will get less priviliges, not more).

What might be a problem is new sites, that do not make their uid 1 special (I'm not sure how the current patch handles this situation). Should they install one such module that does give uid 1 special powers, that would indeed be a security risk.

I wonder whether this is something that should be handled the same as API deprecation as much as possible. Obviously, there is no actual API to mark as deprecated, unfortunately, but the suggestion of adding this check to coder is probably a good one. Also, modules that do this will need to get the same treatment when a new security issue is found, at some point, I suppose. If they indeed allow certain functionality based on the user being logged in as uid 1 which should really be based on assigned roles. Maybe that point should be when they claim to be compatible with Drupal 9, with the background of treating it as any deprecated API. Also, maybe it means that uid = 1 should also get the admin role on new sites and that we may need to prevent uid=1 from being stripped of its god role just yet. Or, display a warning if it is and optionally provide a configuration key to turn that warning into an informational message.

Anonymous’s picture

IMO, the fact that we can not fix this flaw for many years (11!) can not be an argument that it should remain so. And if the modules use magic numbers, instead of check of permissions, these are their problems. I've never seen Drupal recommend doing such checks.

#174:

I wonder how big that problem is

I downloaded all available modules for 8.x and checked various cases with uid/id/id() ===? 1. Near 37/4166 modules check user/1.

And it's really terrible! Because the examples of use really once again emphasize that this flaw needs to be eliminated as quickly as possible.

Random examplse:

  1. This provides part of their functionality for user/1 instead of admin users:
    http://cgit.drupalcode.org/simple_fb_connect/tree/src/SimpleFbConnectUse...
    protected function loginDisabledForAdmin(User $drupal_user) {
        // Check if current user is admin.
        if ($drupal_user->id() == 1) {
    
          // Check if admin FB login is disabled.
          if ($this->configFactory
            ->get('simple_fb_connect.settings')
            ->get('disable_admin_login')) {
    
            $this->loggerFactory
              ->get('simple_fb_connect')
              ->warning('Facebook login for user @user prevented. Facebook login for site administrator (user 1) is disabled in module settings.', ['@user' => $drupal_user->getAccountName()]);
            return TRUE;
          }
        }
    
        // User is not admin or admin login is not disabled.
        return FALSE;
      }
    
  2. This should use own permissions instead of magic number:
    http://cgit.drupalcode.org/photos/tree/photos.module?h=8.x-4.x#n53
          // Value is fid, check if user can view this photo's album.
          if ($user->id() == 1) {
            return TRUE;
          }
    
  3. This based on non-existent super admin:
    http://cgit.drupalcode.org/search_config/tree/search_config.node.inc?h=8...
      // Return the form for super admin unchanged
      if ($user->id() == 1 && !empty($settings['restrictions']['admin_bypass'])) {
        return $form;
      }
  4. This just for prevent user/1 mode.=:
    http://cgit.drupalcode.org/profile/tree/tests/src/Kernel/ProfileRoleAcce...
        // Do not allow uid == 1 to skew tests.
        $this->createUser();

Full list of files where this was found (some modules are obviously outdated):

Should they install one such module that does give uid 1 special powers, that would indeed be a security risk.

Yes, it will be a problem on sites where user/1 will change on no-admin and used one of these unsecure-modules. Now I understand what point @David_Rothstein and @heddn. But contrib modules are always full of security flaws, and we have a time of wide notification about this.

eelkeblok’s picture

Thanks for doing that investigation.

But contrib modules are always full of security flaws, and we have a time of wide notification about this.

That's too easy, though :) From one day to the next, apparently 37 modules will have a security issue. Going through a few entries in your list, there's at least a few that are covered by the security policy, so they will either need a security update at some point (which point? There is probably going to need to be a deadline, since this is basically a zero-day issue of our own creation), or they will need to be marked as abandoned.

Edit: I just deleted two paragraphs about handling this as a deprecation because I realised there is an essential difference with stuff that is deprecated; that is usually the previous "right" way of doing things. If this ever was the "right" way of doing things, that's a long time ago. So I guess it is basically about determining the deadline for module developers to fix their modules if they want to keep their security team support.

eelkeblok’s picture

(Removed, irrelevant).

andypost’s picture

+++ b/core/lib/Drupal/Core/Cache/Context/UserRolesCacheContext.php
@@ -27,13 +27,6 @@ public static function getLabel() {
   public function getContext($role = NULL) {
-    // User 1 does not actually have any special behavior for roles; this is
-    // added as additional security and backwards compatibility protection for
-    // SA-CORE-2015-002.
-    // @todo Remove in Drupal 9.0.0.
-    if ($this->user->id() == 1) {
-      return 'is-super-user';

Also some contrib could rely on "is-super-user" cache context value

rootwork’s picture

37 modules seems like a manageable thing, to the point that we could just contact all of them and point them to a change record with proper code they could drop in (i.e. even if they don't have security coverage). This doesn't seem like a reason to me not to fix this.

eelkeblok’s picture

I don't think anyone said it's a reason not to fix this problem.

kristiaanvandeneynde’s picture

Also, maybe it means that uid = 1 should also get the admin role on new sites and that we may need to prevent uid=1 from being stripped of its god role just yet. Or, display a warning if it is and optionally provide a configuration key to turn that warning into an informational message.

UID 1 will receive the admin role on new sites, it's in the patch. We discussed adding a warning or preventing the removal of a role in this issue before and even the usability review thought it was fine to not bother.

From one day to the next, apparently 37 modules will have a security issue.

No, they already have one. One we're conveniently masking because UID1 = god and one that will continue to be masked because the update hook in the patch makes sure UID1 will receive the admin role post-update.

Also some contrib could rely on "is-super-user" cache context value

Which is why the context isn't going anywhere. It's merely returning a value which is consistent with the new behavior. It's basically asking whether the cache object needs to vary by super user. Seeing as we no longer have one, it makes sense for the context to always respond with "no".

@vaplas in #175: Awesome detective work! <3

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

rivimey’s picture

... and another long break :(

If the thing about possible security holes is the reason, how about this as an idea:

For an upgrade from any existing site, uid=1 implies administrator, so no module checking for '1' is going to be surprised by the changed behaviour in this patch.

For a newly set up site, *don't* allocate uid=1 to any user - blacklist it. Allocate the administrator user a uid > 1 and preferably with some degree of randomness. Now, a module checking for uid=1 will be perfectly safe, because uid=1 is never used. Of course, that module's admin functions won't work, but that is safe, and suitable warnings/docs can educate people what happened.

eelkeblok’s picture

I've been back and forth on some long post, as I tend to do, but the central point is every time: shouldn't we just involve the security team to coordinate security releases for this "dirty 37"?

Part of me thinks we need to somehow handle this as an API change, with deprecation and all that. The problem is, there is no obvious way to "deprecate", with IDE support and such. In practice, I think it has been "deprecated" for years now, it was just never communicated clearly and core having it baked right in didn't help, obviously. So, I think "deprecating" the practice can't really take any other form then making sure uid=1 will be as close to all-powerful as it ever was, without harcoding it. Just like this patch is doing. The rest needs to be [making] noise [about the core change]; release notes stressing this point and a clear change record. Also, if it isn't already, Coder should probably start scanning for it.

So, next steps:

  1. Involve security team
  2. Review change record (as far as that hasn't happened yet)
  3. Figure out how this can be integrated into Coder module (should probably go in the Drupal Practice profile?)

Make sense?

eelkeblok’s picture

One other thing: @vaplas, would you be able to turn your investigation into a script, so it becomes repeatable and we can keep the list current, and maybe even automate the creation of security reports?

Rob C’s picture

I think it has been "deprecated" for years now, it was just never communicated clearly

Can't agree more, but.

The current list is so small and only contains a handful of highly used modules. so lets:
1. Update the patch(es)?
2. Create / edit / amend changerecords / (port)guides / etc.
3. Inform all modules in 8.x that implement uid1 it will be deprecated (can start on this one right now, admin_menu and og still using uid1 should really be fixed i guess).
4. Agree on the change / get confirmation from maintainers + set a date/version?
5. Commit

Hey i love disabling uid 1 on all the sites i worked on, starting to be a habit / sport / hobby, not sure yet. But uid1 is just a pain on sites where security needs to be tight. And lots of us have seen it before with for example feature / ctools exports & language breaking, (etc). Things happen or worse do not happen cause it's uid 1 (or not). (Juniors have to dig for hours to understand that logic.)

Once it's committed it will break some modules, so people will need to move anyway. The longer we wait now the more modules will implement it again, cause that's how we did it right? (and if i check the docs now it's still not clear enough, so people will again). I would like to call for a lil focus on the issue.

And we can do the deprecation dance, but it's such a special case, and it's silently deprecating for some time, so:

@security team & Subsystem maintainer (Moshe ?) : please review this issue for viability in 8.x (i fear...)

eelkeblok’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Needs subsystem maintainer review

This is probably the correct way to get some attention from the subsytem maintainer. Not sure about the security team.

The last submitted patch, 162: drupal-540008-162.patch, failed testing. View results

tstoeckler’s picture

Status: Reviewed & tested by the community » Needs work

RTBC really means there is a patch to be committed, which I don't think is the case here. Otherwise maybe prefix the issue with "[policy]", but even then it seems the more appropriate status would be "Needs review" not RTBC.

eelkeblok’s picture

Status: Needs work » Needs review

Sorry, you're right. Somehow I remembered this patch had reached RTBC before and had been demoted on the grounds of policy-type doubts. I think #178 was addressed in #181, so the more accurate status would be "Needs review" again.

eelkeblok’s picture

Issue summary: View changes

Updated summary (added to section "Potential problems with not special casing" and added Remaining tasks).

eelkeblok’s picture

eelkeblok’s picture

Issue summary: View changes

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

ndf’s picture

Another upvote for removing special behavior of user-1!

While debugging a custom access implementation of hook_ENTITY_TYPE_access() and NodeAccessControlHandler I stumbled again on the fact that user 1 always all permissions including bypass node access.

@see: html/core/lib/Drupal/Core/Session/UserSession.php

  /**
   * {@inheritdoc}
   */
  public function hasPermission($permission) {
    // User #1 has all privileges.
    if ((int) $this->id() === 1) {
      return TRUE;
    }

    return $this->getRoleStorage()->isPermissionInRoles($permission, $this->getRoles());
  }
esolitos’s picture

Version: 8.9.x-dev » 9.0.x-dev
xjm’s picture

Version: 9.0.x-dev » 9.1.x-dev

Since this is disruptive, it'd be a minor-only change.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

daffie’s picture

Title: [PP-1] Remove the special behavior of uid #1. » Remove the special behavior of uid #1.
daffie’s picture

Status: Needs review » Needs work

Some code was added in #2917584: Some tests only go green because they happen to run as UID1, that needs to be removed in this issue.

Spokje’s picture

FileSize
38.3 KB
53.68 KB

Let's start with a re-roll of #169 against 9.2.x. (Or at least, a first stab at it, including removal of added code in #2917584: Some tests only go green because they happen to run as UID1 as correctly pointed out by @daffie)

daffie’s picture

+++ b/core/lib/Drupal/Core/Cache/Context/IsSuperUserCacheContext.php
@@ -8,6 +8,8 @@
+ * @deprecated in Drupal 8.5.0 and will be removed before Drupal 9.0.0.

The deprecation will be in 9.2. The release of 8.5 was some time ago.

Spokje’s picture

Assigned: Unassigned » Spokje

The deprecation will be in 9.2. The release of 8.5 was some time ago.

I wish that was the only problem with reroll... ;)

Seriously: Thanks, I'll fix it together with the upcoming Custom Commands fixes.

Spokje’s picture

phpcs changes (which "accidentally" also caught @daffie's remark)

Spokje’s picture

FileSize
689 bytes
53.59 KB

Fixing the fixes...

Spokje’s picture

FileSize
7.52 KB
61.45 KB

Less failures.

Spokje’s picture

Sorry for the noise in here.

Spokje’s picture

FileSize
2.49 KB
65.06 KB

Once more unto the breach, dear friends!

Spokje’s picture

Assigned: Spokje » Unassigned

This is as far as I can take this patch.

daffie’s picture

My first review of the patch. Will do another one later.

  1. +++ b/core/lib/Drupal/Core/Cache/Context/IsSuperUserCacheContext.php
    @@ -8,6 +8,10 @@
    + * @deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. See
    + * https://www.drupal.org/node/2910500 for more information.
    + * @see https://www.drupal.org/node/2910500
    

    We are deprecation a whole class. Therefore we need to add a trigger_error() and a deprecation message test. For an example see: #3174662: Encapsulate \PDOStatement instead of extending from it.

  2. The Drupal\FunctionalTests\Installer\InstallerExistingConfigSyncDirectoryMultilingualTest can be fixed by replacing the line: $this->assertSession()->linkExists('Add content'); with $this->assertSession()->responseContains('Congratulations, you installed Drupal!');.
  3. The Drupal\KernelTests\Config\DefaultConfigTest can be fixed by inserting the line: $this->installEntitySchema('user'); just before the line $this->installConfig(['system', 'user']);.
  4. The Drupal\Tests\node\Kernel\Views\NodeViewsFieldAccessTest can be fixed by adding in the method Drupal\Tests\views\Kernel\Handler\FieldFieldAccessTestBase::setUp() the permission 'access content' to the $role_with_access.
  5. In the file core/modules/node/tests/src/Kernel/Views/NodeViewsFieldAccessTest.php we have the function workspaces_install(). In that function we have the code:
    // Default to user ID 1 if we could not find any other administrator users.
    $owner_id = !empty($result) ? reset($result) : 1;
    

    We no longer have default administrator user id, therefore the default value should be 0 and the added comment need to be changed.

  6. +++ b/core/modules/user/tests/src/Kernel/UserInstallTest.php
    @@ -48,6 +49,10 @@ public function testUserInstall() {
    +    $rids = \Drupal::database()->query('SELECT roles_target_id FROM {user__roles} WHERE entity_id = 1')->fetchCol();
    

    Can we change this query to:

    $rids = \Drupal::database()->select('user__roles', 'r')
      ->fields('r', ['entity_id'])
      ->condition('entity_id', 1)
      ->execute()
      ->fetchCol();
    
  7. +++ b/core/modules/user/user.install
    @@ -97,3 +104,38 @@ function user_install() {
    +function user_update_8101() {
    

    Can the function be renamed to user_update_9201.

  8. +++ b/core/modules/user/tests/src/Functional/Update/UserUpdateUserOneAdminTest.php
    @@ -0,0 +1,106 @@
    +   * user_update_8101_legacy_admin_role setting in settings.php.
    ...
    +    return Settings::get('user_update_8101_legacy_admin_role', 'administrator_legacy');
    
    +++ b/core/modules/user/user.install
    @@ -97,3 +104,38 @@ function user_install() {
    +  $legacy_admin_role = Settings::get('user_update_8101_legacy_admin_role', 'administrator_legacy');
    
    +++ b/core/modules/user/user.post_update.php
    @@ -13,3 +17,30 @@ function user_removed_post_updates() {
    +  $legacy_admin_role = Settings::get('user_update_8101_legacy_admin_role', 'administrator_legacy');
    

    Can we rename the setting to: user_update_9201_legacy_admin_role.

Spokje’s picture

FileSize
5.73 KB
68.01 KB

Thanks (as always) to @daffie for the review, especially for the sharp eye in spotting the references to when this was a D8.x patch.

Attached patch (should...) address all remarks but

1.

+++ b/core/lib/Drupal/Core/Cache/Context/IsSuperUserCacheContext.php
@@ -8,6 +8,10 @@
+ * @deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. See
+ * https://www.drupal.org/node/2910500 for more information.
+ * @see https://www.drupal.org/node/2910500

We are deprecation a whole class. Therefore we need to add a trigger_error() and a deprecation message test. For an example see: #3174662: Encapsulate \PDOStatement instead of extending from it

from #212.

Spokje’s picture

Fixing a fix...

Point 1. in #212 still needs addressing.

kristiaanvandeneynde’s picture

Thanks for picking this up. I put a ton of effort into it a few years ago so it warms my heart to see progress once again.

Spokje’s picture

Status: Needs work » Needs review
FileSize
5.23 KB
72.83 KB

Stab at

1.

+++ b/core/lib/Drupal/Core/Cache/Context/IsSuperUserCacheContext.php
@@ -8,6 +8,10 @@
+ * @deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. See
+ * https://www.drupal.org/node/2910500 for more information.
+ * @see https://www.drupal.org/node/2910500

We are deprecation a whole class. Therefore we need to add a trigger_error() and a deprecation message test. For an example see: #3174662: Encapsulate \PDOStatement instead of extending from it

daffie’s picture

Status: Needs review » Needs work

The patch looks good. I have some minor remarks and I did not look at the deprecation tests. That I do do later.

  1. +++ b/core/modules/user/src/RoleInterface.php
    @@ -22,6 +22,11 @@ interface RoleInterface extends ConfigEntityInterface {
    +   * Role ID for the admin role; should match what's in the "role" table.
    

    Could we change the part should match what's in the "role" table. to should match the 'role' entity ID.

  2. +++ b/core/modules/user/tests/src/Functional/Update/UserUpdateUserOneAdminTest.php
    @@ -0,0 +1,106 @@
    +    $this->assertTrue($this->userOneHasAdminRole());
    +    $this->runUpdates();
    +    $this->assertTrue($this->userOneHasLegacyAdminRole(), 'User one received the legacy admin role.');
    

    Could we before the updates are run test that user 1 does not have the legacy admin role and could we test that after the updates have run that user 1 does not have the admin role.

  3. +++ b/core/modules/user/tests/src/Traits/UserCreationTrait.php
    @@ -306,8 +306,7 @@ protected function checkPermissions(array $permissions) {
    -        $this->fail(new FormattableMarkup('Invalid permission %permission.', ['%permission' => $permission]), 'Role');
    -        $valid = FALSE;
    +        $this->fail(new FormattableMarkup('Invalid permission %permission.', ['%permission' => $permission]));
    

    This change look wrong to me. Could we try a patch without this change.

  4. +++ b/core/modules/user/user.install
    @@ -97,3 +104,38 @@ function user_install() {
    +  // We need to be careful not to grant people more access all of the sudden.
    +  // Migrate the administrator role to a new administrator_legacy role. We will
    +  // assign old administrators that role in the post update hook.
    +  $legacy_admin_role = Settings::get('user_update_9201_legacy_admin_role', 'administrator_legacy');
    

    We can move these lines into the next if-statement. The variable $legacy_admin_role is not used anywhere else. It also improves the readability of the function.

  5. +++ b/core/modules/user/user.install
    @@ -97,3 +104,38 @@ function user_install() {
    +  if ($role->isNew() || isset($legacy_role)) {
    

    I think we should initialize the variable $legacy_role at the beginning of the function as it could be not set.

anmolgoyal74’s picture

Status: Needs work » Needs review
FileSize
72.26 KB
3.12 KB

Addressed #217.1,#217.2,#217.3, and #217.4.

for #217.2
We are adding admin role to user 1 after the update.

diff --git a/core/modules/user/user.post_update.php b/core/modules/user/user.post_update.php
+  $account = User::load(1);
+  $account->addRole('administrator');
+  $account->save();

So we cannot test for user 1 does not have the admin role. I have added the test to check if user 1 has admin role. not sure if required.

for #217.5, I didn't understand what to change.

daffie’s picture

Status: Needs review » Needs work

After these changes it is for me RTBC.

  1. +++ b/core/modules/user/user.install
    @@ -97,3 +104,38 @@ function user_install() {
    function user_update_9201() {
      $config_factory = \Drupal::configFactory();
      $config_name = 'user.role.administrator';
      $role = $config_factory->getEditable($config_name);
    +  $legacy_role = NULL;
    
    

    To explain #217.5: When the first if-statement results into false the variable $legacy_role does not get set. I would like to have that variable be initialized before we are testing it in the second if-statement.

  2. +++ b/core/lib/Drupal/Core/Cache/Context/IsSuperUserCacheContext.php
    @@ -3,18 +3,32 @@
    + * @deprecated in drupal:9.2.0 and is removed from drupal:10.0.0. See
    + * https://www.drupal.org/node/2910500 for more information.
    + * @see https://www.drupal.org/node/2910500
    

    Can we have an empty line before the line with @see.

  3. +++ b/core/lib/Drupal/Core/Cache/Context/IsSuperUserCacheContext.php
    @@ -3,18 +3,32 @@
       public static function getLabel() {
    +    @trigger_error('Calling ' . __METHOD__ . '() is deprecated in drupal:9.2.0 and will be removed in drupal:10.0.0. See https://www.drupal.org/node/2910500 for more information.', E_USER_DEPRECATED);
    
    @@ -22,13 +36,17 @@ public static function getLabel() {
       public function getContext() {
    ...
    +    @trigger_error('Calling ' . __METHOD__ . '() is deprecated in drupal:9.2.0 and will be removed in drupal:10.0.0. See https://www.drupal.org/node/2910500 for more information.', E_USER_DEPRECATED);
    ...
       public function getCacheableMetadata() {
    +    @trigger_error('Calling ' . __METHOD__ . '() is deprecated in drupal:9.2.0 and will be removed in drupal:10.0.0. See https://www.drupal.org/node/2910500 for more information.', E_USER_DEPRECATED);
    
    +++ b/core/tests/Drupal/Tests/Core/Cache/Context/IsSuperUserCacheContextTest.php
    @@ -0,0 +1,66 @@
    +  public function testGetLabel() {
    ...
    +  public function testGetContext() {
    ...
    +  public function testGetCacheableMetadata() {
    

    Having the deprecation trigger in the __construct method is enough. The methods getLabel(), getContext() and getCacheableMetadata() do not need their own deprecation triggers. The deprecation message testing for those methods is also not needed.

  4. +++ b/core/lib/Drupal/Core/Session/PermissionsHashGenerator.php
    @@ -42,11 +50,14 @@ class PermissionsHashGenerator implements PermissionsHashGeneratorInterface {
    +    $this->entityTypeManager = $entity_type_manager;
    

    Before this line the following code needs to be added:

    if ($entity_type_manager === NULL) {
      @trigger_error('Calling ' . __METHOD__ . ' without $entity_type_manager argument is deprecated in drupal:9.2.0 and will be required in drupal:10.0.0. See https://www.drupal.org/node/2910500', E_USER_DEPRECATED);
      $entity_type_manager = \Drupal::entityTypeManager();
    }
    

    There is no deprecation message testing needed for this change.

daffie’s picture

Issue summary: View changes

Updated the Is with 2 API changes.

Edit: Also updated the CR with the same API changes

Spokje’s picture

Assigned: Unassigned » Spokje

Thanks @daffie (as usual), new patch incoming.

Spokje’s picture

Status: Needs work » Needs review
FileSize
5.48 KB
69.5 KB

Attached patch should address all the points raised by @daffie in #219.

Spokje’s picture

Assigned: Spokje » Unassigned
daffie’s picture

Status: Needs review » Reviewed & tested by the community

After multiple reviews and fixes are all code changes good enough for me.
The patch does what the proposed solution part of the IS is saying.
There is a great CR associated with this patch.
There is also a page added in the docs. See: https://www.drupal.org/node/2913822.
All the changes have testing for them.
The added code block from #2917584: Some tests only go green because they happen to run as UID1 have been removed.
For me it is RTBC.

Thanks to everybody who worked on this issue!

NitinLama’s picture

Was already working on the patch, thanks @Spokje for the patch. @here adding patch with some little comments to complete it as per #219 @daffie

NitinLama’s picture

Status: Reviewed & tested by the community » Needs review
Spokje’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
4.12 KB

Thanks @NitinLama, but @daffie already RTBC-ed patch #222 (which addressed all of his concerns in #219).

Also your patch reintroduces the deprecation message tests for getLabel(), getContext() and getCacheableMetadata() which @daffie asked to be removed in #219.3:

The methods getLabel(), getContext() and getCacheableMetadata() do not need their own deprecation triggers. The deprecation message testing for those methods is also not needed.

(See my interdiff, which is different than the one you've uploaded.)

Hiding your patch so #222 will get auto-retested every 2 days and setting this back to RTBC.

daffie’s picture

FileSize
69.5 KB

@NitinLama: Thank you for working on a patch for this issue. The problem for me is that I already have reviewed the other patch and that one is for me RTBC. For the next time, when you are wanting to work on an issue, please first assign the issue to yourself. Like @Spokje did. Reviewing is hard work and I do not like to do the same work twice.

I am reuploading the patch from comment #222 which for me is RTBC.

NitinLama’s picture

@daffie noted.

The last submitted patch, 225: 540008-225.patch, failed testing. View results

daffie’s picture

Testbot failure for the patch from comment #225.

johnwebdev’s picture

Status: Reviewed & tested by the community » Needs review

If you had a user role with the machine name 'administrator' on your website that was not, in fact, an admin role (i.e.: did not have all permissions), then that role will be migrated to an 'administrator_legacy" role. All users who had the "non-admin administrator role" will be updated to use the legacy administrator role.

This could potentially break stuff, if you have code that check for that role specifically ->hasRole('administrator')

Grep: http://grep.xnddx.ru/search?text=hasRole%28%27administrator%27%29&filename=

greggles’s picture

Status: Needs review » Reviewed & tested by the community

This could potentially break stuff, if you have code that check for that role specifically ->hasRole('administrator')

Code that relies on that magic string is a bug and should be fixed. This will be a migration task to address, but it will be a short-lived one.

I think the Change Record addresses that problem and the xnddx search you posted will make it easy to file issues in contribs. I'm happy to file issues like that to get this security improvement shipped.

Berdir’s picture

That string is no more magic than any other config entity id and while generally not recommended, it *is* perfectly valid to have role based checks, in custom projects it's sometimes not easy to avoid.

I'm not convinced that renaming the existing role is the right approach here. We have no idea what else is tied to that, What about views with access rules for roles or routes, the add/remove actions and so on?

Why can't we add the the new role as a different ID? If we can ensure anything it's that *core* should not rely on a magic ID, not the reverse.

eelkeblok’s picture

First of all, let me say how excited I am this is getting some attention again. Thank you!

@berdir Hm. Well, I guess we can, but that is a pretty fundamental assumption in this issue, AFAICT. I think the reason for "hard coding" the administrator role is that that makes it easier to guarantee there always is an admin role to grant to a user in case of emergency, plus it decomplicates the UI because you no longer need an 'admin role' setting.

I think that keeping the role name fluid means what is considered the system-supported admin role should remain configurable (I actually had to do some digging myself why that change was even in scope of this issue). Could we not ensure there "always" is an admin role available by making the admin role config mandatory and protecting that role from deletion? (We are already creating a default role for new installs, and we have most of the update code in place to make sure there is something sane to set it to on existing installs as well). Of course, people could still break their site by removing the config object, but I don't think the current patch prevents that either. We could consider de-emphasizing the setting; it currently is second in the account settings screen.

To add a personal note, and why I am actually not a fan of the role shuffling either, is that it will definitely wreak some havoc in our projects. We usually have a role 'administrator' meant for key users that does not have every permission under the sun. Just a lot. We then have a 'superadmin' role that is actually configured as the system-supported administrator role.

To be least disruptive, I would actually propose the following flow:

  1. If the site has an admin role configured and it is assigned to user 1, do nothing
  2. If the site has an admin role configured and it is not assigned to user 1, assign it
  3. If the site does not have an admin role configured, and it doesn't have a role named administrator, create such a role, set the admin role setting and assign the role to user 1
  4. If the site does not have an admin role configured, but it does have a role named administrator, all bets are off. Create a role with ID [TBD] and assign it. Also add an update message linking to the change record, that is updated to include a section to guide users through the fact that their site has a role named administrator that is not actually configured to be that.

So that last one is the real stinker (we already actually discussed the naming of a new role above, I am partial to 'magical_sparkling_pony'). I think this situation will be somewhat of an edge case, though. Let's recap:

  • There is a role named 'administrator'
  • That role is not configured to be the actual, system-supported administrator role
  • There is no other role configured to be the system-supported administrator role

In that case, I think it is acceptable to pick an ugly name such as 'user_update_9201_admin_role', and label 'Administrator (User update 9201)' (not in the least to make it googleable). Like I said above, I would suggest to include a message linking to the change record, that can contain some guidance on how to improve the - perfectly functional, but no so pretty - situation. Having the admin role setting available would help here too; instructions to replace the automatically created role with either a role that was already used for the purpose, just misconfigured or creating an entirely new role with a project-apropriate name would become fairly trivial. And of course, this should als prominently show how to *prevent* this from happening, e.g. by using the update setting to set the role name, or preparing by properly configuring an admin role. Which should be linked to from the release notes, to hopefully further minimize the number of sites that end up in situation 4. The recovery instructions would become only slightly more complicated, as they would involve finding out what the ID is of the admin role.

Berdir’s picture

I understand this is tricky. It is somewhat ironic to compare #233 (magical strings are bad) vs #235 (magical strings make everything easier) tough :)

Didn't read every detail but yeah, something like that could work. Because I don't quite see how you'd get out of having a non-standard role as we don't support to rename them (exactly because of actions and views and ...). And not allowing to delete the configured admin role makes sense, just like you can't delete uid 1 (afaik). Not sure how strictly we want to enforce that (just validation vs throwing exception. Not sure if there is an edge that could be fun like a config deployment that changes and deletes the old role at the same time).

We have the admin vs super admin thing too, but we keep administrator and call the other one client_admin. But isn't #4 exactly what would happen in your case? So it's not that edge-casy? What kind of guiding would you need, it's not like you need to fix it if we do keep the setting?

eelkeblok’s picture

No, my case is #1, possibly a #2 here or there; we have a configured admin role, it is just not called ‘administrator’.

I’d imagine the guidance being a howto of creating a new role, reconfiguring to make it the system-managed admin role, assigning it to user 1 and removing the auto-created role. Only relevant for #4, and really only when there is no way to start over, because if it is at all feasible to rerun the update (i.e. ran into this on a dev system), using the setting to set the id for the new role for the update, or configuring an admin role up front, would be preferable.

kristiaanvandeneynde’s picture

I'm with greggles in #233: If you have code checking for the administrator role -which you really shouldn't, but as Berdir pointed out, can be necessary sometimes- then all you need is a simple search and replace in your custom code.

The only thing that I did not initially think of when working on this patch, was entity reference fields outside of the user roles field. I'm not necessarily setting the issue back to needs work because:

  1. If you have views or other config checking access based on roles, that's your mistake
  2. If you use entity reference fields to check for roles and grant access based on that, it's also bad practice and therefore your mistake
  3. We should not account for people writing insecure code like 1. and 2.

But, I might be missing something where ER fields pointing to roles is a big issue. I'd even argue that the mere fact it used to point to the "right" role and now might not (i.e. points to "new" admin rather than original one) is a data change that people might not expect.

So still leaning towards RTBC but slightly worried about ER fields all of a sudden pointing to a different config entity. Note that this would not be an issue if the ER fields were using UUIDs.

Berdir’s picture

#237: Yeah sorry, I only realized half into my comment that you suggested to not remove or deprecate the setting but keep it, so yes, you are option 1.

#238: That's simply not how our BC policy works. We can *not* break stuff like views with role access configuration (which is perfectly secure, and in some cases even more flexible as you can select multiple roles but not multiple permissions). We'd also possibly break things like https://www.drupal.org/project/rac, block visibility conditions (which you can only do with roles and not permissions) and countless other things. None of that is by definition insecure and there is no way we can update and adjust all those possible references.

kristiaanvandeneynde’s picture

Re #239 you convinced me. There are too many references to fix. In that case, this is needs work and I fully agree with #235 points 1 through 3 and I think the suggestion for point 4 is fair, even though it might not please everyone.

Flagging an existing misconfigured admin role is still better than swapping 2 roles and unleashing all sorts of unexpected entity reference failures. So let's get rid of the update hook that swaps out roles and go with #235? I also support its suggestion of a very "Googleable" name.

rivimey’s picture

Late to the party, but IMO
(a) totally agree that role shuffling is bad and not at all sure why role-based access checks might ever be considered a mistake;
(b) I think in the difficult case 4, the update should abort and do nothing, perhaps setting a flag somewhere that causes a message on admin pages indicatng the problem. Rerunning the update would be needed once the user has fixed the conflict.

Basically, I don't like the idea of the update resulting in having strange role names in websites forever more, which seems to be the net result of the policy @berdir proposes, given you "can't" rename the machine name of a role.

[I do wonder if that policy can be relaxed somewhat, but I do see why its there].

quietone’s picture

I came here to see how this may have changed the migrate_drupal_ui functional tests, which use user 1. There are no changes but I think they should be changes to the comments that refer to using user 1. That make me which that this should have a follow up to tweak any comment referring to 'user 1'. For example, https://git.drupalcode.org/project/drupal/-/blob/9.2.x/core/modules/migr... isn't quite right anymore. Just a thought.

kristiaanvandeneynde’s picture

not at all sure why role-based access checks might ever be considered a mistake;

Consider you have an editor role and that editor has to have specific access to all sorts of custom made dashboards, custom content entities, etc. and you tie ALL of that to the fact they have a role. Now your client asks for a junior editor role. Well, congratulations, you just bought yourself hours of scouring the codebase for role checks whereas if you had used permissions from the start, introducing the new role would have taken minutes.

Vice versa it's even worse: Consider one of these roles had too much access and for security reasons needs to have some access removed immediately. Rather than unticking a checkbox or two, you can ask a developer to check the entire codebase and pray they found all offending code.

THAT is why you should never use role based access control unless you absolutely know what you're doing.

eelkeblok’s picture

Status: Reviewed & tested by the community » Needs work

Basically, I don't like the idea of the update resulting in having strange role names in websites forever more, which seems to be the net result of the policy @berdir proposes, given you "can't" rename the machine name of a role.

I completely relate to this sentiment, that is why #4 is an absolute last resort. I do think we need it, because we will need to make sure that uid 1 is still fully privileged afterwards; remember, the code will be updated, so uid 1 will not be special afterwards, all we can do to make sure people can still use uid1 to do what they are used to is to assign it a fully privileged role. If we do not take automated action, we might inadvertently lock people out of their site. Scenario's 1, 2 and 3 are meant to do that in a recommended way, with as little change as possible. If all of them do not apply, scenario 4 is all that is left. Skipping it doesn't seem an option to me.

Also, let's not forget we'll do everything in our power to point people to instructions to either fix or avoid scenario 4. It doesn't need to be "forever more", it should be possible to recover from that situation. Just not in an automated fashion, because it will need a human to decide what is the most appropriate action (create a new role and if so, how to name it, or promote an existing role to the system admin role). That still means there will probably be many sites out there with that ugly automated role name. However, it does beat all those people, with sites that would otherwise end up with that ugly role, generating a flood of support requests because they can not get into their sites anymore, IMHO :)

BTW, I think we have some consensus that we do need to do something about this role shuffling situation, so putting back to "Needs work".

Spokje’s picture

Adding Needs issue summary update tag flagging that the approach that will be taken in the 4 outlined scenarios needs to be added to the IS.

Also I think we need to address the issue about incorrect comments about UID 1-users flagged by @quietone in #242.
Not sure if that needs to be done in here or in a follow-up myself, but I'm sure there are Bigger Brains in this issue that can decide on that.

Spokje’s picture

And of course the change-record will need an update for the 4 scenarios and instructions on how to avoid/fix scenario 4. Tagging accordingly.

catch’s picture

#540008-235: Add a container parameter that can remove the special behavior of UID#1 seems like a good plan. I think having the clear instructions and also an easily googleable 'strange' role name is not a bad idea - since someone who misses the entire thing and finds it one day months later could then search and find the change record.

johnwebdev’s picture

This patch implements and tests first three cases in #235

eelkeblok’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

Updated issue summary. I am not 100% sure about the status of the remaining tasks

daffie’s picture

Status: Needs work » Needs review
FileSize
5.73 KB
68.82 KB

Added the 4th case from comment #235 and added testing for it.

daffie’s picture

Issue summary: View changes
Issue tags: -Needs change record updates

Updated the IS and the CR.

kristiaanvandeneynde’s picture

+++ b/core/modules/user/tests/src/Functional/Update/UserUpdateUserOneAdminTest.php
@@ -65,6 +65,45 @@ public function testUserOneGetsNewAdminRoleAssigned() {
+

Not sure why the data provider or removing the role. The whole point is to introduce a special role if there is a role named 'administrator' that does not have admin rights.

So in this case, whether or not UID1 had the 'administrator' role does not matter. Is it there for mere hardening?


Rest of the interdiff looks great.

daffie’s picture

So in this case, whether or not UID1 had the 'administrator' role does not matter. Is it there for mere hardening?

Yes, we want to make sure that the UID1 get assigned a role that has the right privileges.

kristiaanvandeneynde’s picture

Looks good to me then.

eelkeblok’s picture

Status: Needs review » Needs work

Thanks a lot for taking that one. I have two remarks.

  • Although I get the deviation from the suggested role ID and label (didn't realise we'd moved to a hook_post_update when I made that suggestion), I worry that "Administrator role post update" is much less Googleable; they are all terms that are fairly common in Drupal. What if we change it to machinename user_1_admin and label "User 1 admin role (auto-generated)"? I think we need to make it abundantly clear this thing was auto-generated. Resist the urge to make it less ugly :)
  • The change record should include instructions on what to do when you do end up with the auto-generated role. In short:
    1. Review if in fact the existing administrator role could be configured as the system-supported administrator role, or whether there is another role that fits that purpose
    2. If not, create a new role of your choosing for this purpose
    3. Assign user 1 (or another user you have access to) the role of choice if they don't have it yet
    4. Change the admin role setting to the role of choice
    5. Delete the auto-generated role
kristiaanvandeneynde’s picture

Issue summary: View changes
FileSize
140.46 KB

Agree with both points of #255

Resist the urge to make it less ugly :)

I vote for "Magical sparkling pony admin (auto-generated)" because:

  1. It's funny
  2. It's easily searchable and less likely to have collisions
  3. Have I mentioned it's funny?

Let's do it.

ndf’s picture

Although I really like the funny suggestion, I would prefer a role-name that describes what is different from the existing administrator role.
And without being offensive or opinionated.

If the site does not have an admin role configured, but it does have a role named administrator, all bets are off. Create a role with ID [TBD] and assign it. Also add an update message linking to the change record, that is updated to include a section to guide users through the fact that their site has a role named administrator that is not actually configured to be that.

What about creating a new role named "administrator_full_priviliged"?
Other words I was thinking about were "root", "super", "sudo". But these don't have enough context in a website in my opinion.

eelkeblok’s picture

I think you're making assumptions about what the existing administrator role is, at this point. Maybe it is fully privileged, but for whatever reason it is not configured to be the system supported admin role. This is why we need this thing to stand out like a sore thumb; we want people to go and find out what on earth this weird role is that showed up in their system. Only they can decide what the best way forward out of the situation is. I think the naming needs to meet a few criteria:

  1. Distinctive enough that the chance somebody happens to have picked the same role ID by themselves is as small as possible (because we are really in trouble then; remember this is our escape hatch, when we are out of options)
  2. Highly Googleable
  3. Obviously auto-generated
  4. Not anxiety invoking ("Help, my website was hacked, some weird role just showed up!")

I think the suggestion from #257 fails on 1 and 3 at least, probably 2 as well. Suggestion in the current patch fails on 2 and 3, possibly 1, although less so. Although, like I said before, I am partial to the sparkling pony, I'm afraid it fails on 4.

ndf’s picture

Maybe it is fully privileged, but for whatever reason it is not configured to be the system supported admin role

You are right, I did not think about that.

I do agree on your criteria.

kristiaanvandeneynde’s picture

#258 +1.

To be fair, are there any names we can come up with that do not match with criteria 1 through 3 and does not incite some form of fear that your site has been hacked? Any "legit" admin-type name will almost always fail on 2 and probably 1 too. Maybe even 3.

I think it's necessary we create a silly, yet amicable name to avoid panic but also check the boxes of criteria 1 through 3. We aren't advocating for "you have been h4xx0r3dz" or anything like that :) We would even have (auto-generated) in the name too to make it more obvious.

Perhaps: "Magical sparkling pony admin (auto-generated by Drupal update 9.2.0)"? To alleviate any fear of being hacked. They would be able to Google the release notes and confirm the legitimacy of the role in under 5 minutes.

johnwebdev’s picture

Magical sparkling pony admin (auto-generated by Drupal update 9.2.0) looks good to me.

1. ✅ It is definitely distinct
2. ✅ Googleable
3. ✅ We explicitly say it's auto-generated. So, yes.
4. ✅ I think we check that by being explicit in 3.

This problem is also reminds me about that PHP constant that exists. That is highly googleable!

johnwebdev’s picture

Found two more occurrences of === 1

MigrateAccessCheck.php:

  public function checkAccess(AccountInterface $account) {
    // The access result is uncacheable because it is just limiting access to
    // the migrate UI which is not worth caching.
    return AccessResultAllowed::allowedIf((int) $account->id() === 1)->mergeCacheMaxAge(0);
  }

Separate issue?

function hook_file_url_alter(&$uri) {
  $user = \Drupal::currentUser();

  // User 1 will always see the local file in this example.
  if ($user->id() == 1) {
    return;
  }

Separate issue. I think we should use another example here, and discourage checking directly for === 1.

daffie’s picture

Status: Needs work » Needs review
FileSize
1.72 KB
69.8 KB

Changed the auto-generated role name to: Magical sparkling pony admin (auto-generated by Drupal update 9.2.0). Also updated the CR.

For #262: Lets do that in a followup issue.

kristiaanvandeneynde’s picture

Not sure if this will work. We want the label to be the whole thing, but IIRC the machine names are limited to 32 characters. So if this fails because of it:

    'id' => 'magical_sparkling_pony_admin',
   'label' => 'Magical sparkling pony admin (auto-generated by Drupal update 9.2.0)',
daffie’s picture

FileSize
1.66 KB
69.69 KB

@kristiaanvandeneynde: Good point.

johnwebdev’s picture

I'm tempted to RTBC, but we still have the "Needs subsystem maintainer review" tag. It's 3 years old that tag. Comment #247 (catch) did give us some validation on the plan, so do we still expect a subsystem maintainer to review at this point, before we RTBC?

seanB’s picture

We have a technical name and a display name for roles. So the technical name can be some auto generated hash thing to avoid collision, but the display name could be something like: "Superadmin (auto-generated by Drupal update 9.2.0)". Even better would be to add a link in the role name to a handbook page/change record explaining the change.

That way we can avoid collisions and directly point users to a place where they can find more information, instead of them having to google it.

It is distinctive in the technical and display name, provides information via a link so users won't have to go look themselves, and avoids the funny role name (which some clients would probably not understand or appreciate).

kristiaanvandeneynde’s picture

Won't putting a link in the role name risk breaking some UIs?

rivimey’s picture

I would vote for the machine name and the display name being fairly close to each other, not wildly different.
And I am very much against the display name having any html in it... as noted it seems likely to break UIs and cause general confusion. Remember (a) tags cannot be nested.

moshe weitzman’s picture

Status: Needs review » Needs work
Issue tags: -Needs subsystem maintainer review

Hi all. I guess I'm the first and last User subsystem maintainer. This patch looks good to me from a subsystem maintainer perspective. From a personal perspective, I think it solves about as many issues as it creates (see issue description). But I think the consensus is to proceed so lets do that. A few minor questions and comments below:

  1. +++ b/core/modules/system/tests/src/Kernel/DateFormatAccessControlHandlerTest.php
    @@ -53,18 +53,10 @@ protected function setUp(): void {
    +    $permissions = ($which_user === 'admin')
    +      ? ['administer site configuration']
    +      : [];
    +    $user = $this->drupalCreateUser($permissions);
    

    Line length coding standard should allow for ternary on 1 line IMO.

  2. +++ b/core/modules/user/src/RoleAccessControlHandler.php
    @@ -20,7 +20,12 @@ class RoleAccessControlHandler extends EntityAccessControlHandler {
    +          RoleInterface::ADMINISTRATOR_ID,
    

    I didn't look too closely but it seems like this method is forbidding admins from deleting roles?

  3. +++ b/core/modules/user/tests/src/Functional/Update/UserUpdateUserOneAdminTest.php
    @@ -0,0 +1,129 @@
    +  protected function setDatabaseDumpFiles() {
    
    +++ b/core/modules/user/user.post_update.php
    @@ -13,3 +18,80 @@ function user_removed_post_updates() {
    +function user_post_update_user_1_with_admin_role() {
    

    Has anyone tested the upgrade when uid=1 is blocked. I would think that it all works fine, and uid=1 is upgraded and remains blocked afterwards. But worth checking.

  4. +++ b/core/tests/Drupal/Tests/Core/Cache/Context/IsSuperUserCacheContextTest.php
    @@ -0,0 +1,27 @@
    +   * Tests deprecation of the \Drupal\Core\Cache\Context\IsSuperUserCacheContext class.
    

    Does this test class merely check that our trigger_error() is working? Maybe I'm missing something.

daffie’s picture

Status: Needs work » Needs review
FileSize
2.59 KB
70.06 KB

@moshe: Thank you very much for your subsystem maintainers review!

For #270-1: Fixed.

For #270-2: That is correct.

For #270-3: Testing added.

For #270-4: That is correct.

xjm’s picture

Issue tags: +Needs usability review

@catch, @Gábor Hojtsy, @laurii, and I discussed this as we all agree we should not use a cute name like "Magical sparkling pony admin". It's confusing, unprofessional, and might make site owners think their site has been hacked.

@catch proposed "Admin (auto-generated by Drupal 9.2.0)". Let's update the patch to use that, and then get a usability review for it. UX meetings are Fridays at 15:00 UTC and kicked off fro the #usability channel in Drupal Slack. Thanks!

xjm credited Gábor Hojtsy.

xjm credited lauriii.

xjm’s picture

Status: Needs review » Needs work

NW for #272. We should also change the machine name to something like autogenerated_admin_9_2_0 or something (use your best judgment).

Also crediting Slack discussion participants.

abhisekmazumdar’s picture

Status: Needs work » Needs review
FileSize
70.02 KB
1.6 KB

I have tried to make the changes as suggested by @xjm. Kindly review.

kristiaanvandeneynde’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me.

Might need a CR update for the role name?

shaal made their first commit to this issue’s fork.

eelkeblok’s picture

I think the CR can do with some more clearing up around the update. And yes, it will need to be updated for the new name. I will try to carve out some time soon to go through it (already in progress, but need to go do something else now).

eelkeblok’s picture

Status: Reviewed & tested by the community » Needs work
eelkeblok’s picture

I updated the CR, mostly fleshing out the different scenario's for existing sites, as well as instructions to prevent the auto-generated role, and recover when you got to that point on a live site.

However, I did find two places where I have some doubts (marked with @todo in the CR);

  • The intro says it is recommended to remove the administrator role from all users after setting up the site. This would suggest we almost expect people to need the recovery instructions when there are no more administrators on the site? Or is it implied the user used to remove the role from "all" users will still retain the role themselves?
  • It is mentioned that the administrator role setting has been removed, but it is a key part to reovering from the scenario with the automatically created role. I haven't checked the latest code yet, so I am not sure what is tru, exactly. I'll see if I can test the functionality with the different scenario's tonight.
daffie’s picture

I have talked to the persons from the usability group (#ux on Slack) and we did not have a lot of time. They said that we should change the proposed name: "Admin (auto-generated by Drupal 9.2.0)" in 2 ways. The first is to change "auto-generated" to "introduced" and the second is to change "Drupal" to the distribution name (which you get from drupal_install_profile_distribution_name()). The new name would become: "Admin (introduced by your distribution name 9.2.0)". I am not sure if we should keep the 9.2.0 part.

johnwebdev’s picture

It feels confusing that we should use the distribution name? It’s not responsible for the change, and even more confusing if you have a custom distribution. It’s also less googleable

eelkeblok’s picture

I just realised there is an extra problem; on sites where the root user does not have any privileges through role permissions (for example, the test site that I just installed using the minimal install profile), it will not be possible to run updates through update.php unless $settings['update_free_access'] was set to TRUE (which should typically not be the case).

Should we add a hack to update.php to give uid 1 access to only the update process? I suppose that will have to remain until Drupal 10, when we can force people to first update to the two minor versions of Drupal 9 prior to 10.0 (which would include this update, so we can be sure of an admin role afterwards)? This does obviously still grant special privileges to a user that otherwise is no longer special, which may be worse than the current situation...

I agree that adding the distribution name in the role label would defeat the purpose of this elaborate name for at least sites that use a distribution.

eelkeblok’s picture

I think this means that actually removing the check for uid == 1 in User::hasPermission() may be one step too far. Instead we should (for now, until D10):

  • move the uid == 1 check to after the actual permission check and trigger a deprecation error when a site relies on it (mostly for catching tests that still rely on this behaviour
  • disallow removing the system admin role from uid 1
  • add a hook_requirement reporting user 1 not having the system admin role as an error (just as a safety net in case somebody does somehow manage to remove the admin role from user 1, e.g. with some custom code)

Then in D10, it is possible to remove those last limits; remove the actual hard-coded check from User::hasPermission() and allow the removal of the role from user 1. And obviously remove the hook_requirement(). Only then can we be sure nobody is updating from a version that would not have a system-supported administrator role available.

Boy, this is messy...

xjm’s picture

This also still needs usability review as I requested in #272 (note that the channel is #ux, not #usability as I previously said).

eelkeblok’s picture

Assigned: Unassigned » eelkeblok

See #282, daffie did get some feedback from the #ux team. Not sure if that constitutes a UX review

daffie’s picture

@eelkeblok: No it does not constitutes a UX review. We will continue next friday, when there is a new UX meeting on zoom.

xjm’s picture

Issue tags: +Needs screenshots

Right, we'll want to implement #282, and then once that's ready, let's have UX review of the updated versions. Screenshots should be added to illustrate each thing that might be changed in the user interface, as documented in the core usability gate. (Make sure to crop screenshots to show only the changed element in context.)

After that, if the changes seems sufficient and no more changes are needed, the usability team can sign off on the final version and remove the tag. Thanks!

eelkeblok’s picture

Well, given #284/#285 I think we are not quite ready for a UX review. I do think it is an important issue that many sites would be forced to set update_free_access to be able to run the update as it is in #276.

I created a merge request to implement #285. I also took a stab at #282, although I did deviate slightly from it; I gave the role the label "Admin (introduced by update process)". The changes against #276 can be seen here: https://git.drupalcode.org/issue/drupal-540008/compare/b9bfbd37f7fdd363f...

  • Made sure the administrator role can not be removed from user 1
  • Restore the admin role configuration, but only show it when it does not match the "default" Administrator role
  • Introduced an error in the status report when user 1 doe snot have an administrator role:
    Error in the status report
  • Introduced a warning in the status report when the system contains the fallback role:
    Warning in the status report

WIll need tests. and I noticed that eventhough I configured a different admin role myself prior to running the update, the "Administrator" role is still being created, which deviates from what I suggested in #235. Maybe always creating the role if it doesn't exist is preferable, since it would mean we could get more sites closer to the "default"; any other admin roles could then just be deleted (after users having it would recieve the new administrator role). However, it does render the text for the change record incorrect. Sorry, that was a mistake, that part is working as designed.

kristiaanvandeneynde’s picture

#285 is a lot of extra trouble for one use case that can easily be circumvented by simply making sure UID1 gets an admin role during the update hook. Up until now they always were admins, so no harm done there.

So let's please stick to the proposed and "approved" version of #282, even though I'm absolutely not a fan of naming it after a distribution. As mentioned before, we want a very "googleable" name and by having it potentially have a thousand different names, that goal is completely obliterated.

kristiaanvandeneynde’s picture

Updated CR to:

  • Fix typos
  • Changed "system-supported" to "all-access" as basically any role is supported by the system so that was confusing
  • Removed a todo

Finally, we should still keep the change that disallows you to change the admin role on the user account form, but @eelkeblok is correct in that it would make it hard to remove the edge case generated role in favor of the access-restricted custom "administrator" role.

Ideally, we should come up with a way (or a script added to the CR) for people to fix that edge case while still making sure all all-access roles are called administrator going forward on clean installs and non-edge case existing installs. This allows for easier documentation and test writing going forward.

catch’s picture

add a hook_requirement reporting user 1 not having the system admin role as an error (just as a safety net in case somebody does somehow manage to remove the admin role from user 1, e.g. with some custom code)

If we deprecate the behaviour rather than removing it, and add this system requirements change, then I think we could probably drop the update altogether and expect site admins to sort it out themselves prior to Drupal 10?

#285 is a lot of extra trouble for one use case that can easily be circumvented by simply making sure UID1 gets an admin role during the update hook.

This isn't true if user 1 tries to run the update, is unable to because it no longer has admin permissions, and therefore doesn't get them.

kristiaanvandeneynde’s picture

If we deprecate the behaviour rather than removing it, and add this system requirements change, then I think we could probably drop the update altogether and expect site admins to sort it out themselves prior to Drupal 10?

Agreed, I'm not in favor of deprecating it. The whole goal of this patch (and why I spent to much time on it) was to finally remove the security risk and Drupalism of user 1. Kicking the can down the road for another year or two would be a major downer.

This isn't true if user 1 tries to run the update, is unable to because it no longer has admin permissions, and therefore doesn't get them.

Yeah I missed that one. But then again, we do have a safety net already with $settings['update_free_access']. If someone can't access update.php because their user 1 did not have an admin role assigned, surely we can simply point them to that workaround rather than implement yet another one? I mean, if not, we kind of admit update_free_access should not be used at all.

I'd rather see this patch go in "clean" with no residue whatsoever, aside from the edge case admin role fix we have now.

eelkeblok’s picture

OK, let's not be "difficult" about the access to the update, then. We will need a clear release note snippet to make sure as many people as possible have at least one user with an admin role (or rather, permission to run updates) to be able to still run updates when they deploy this core version. I am actually not sure what would happen with drush-based updates. Does that need a privileged user too?

One aspect I missed before is that the "admin role" setting is not so much a setting as it is a property of a role (and the setting currently only works when there are 0 or 1 such roles). So, there can be multiple "is admin" roles. I think this means that we could actually simplify scenario's 1, 2 and 3 from #235 into:

  • Make sure the site has a role named administrator. If not, create it, and make it an admin role.
  • Make sure the site's user 1 is assigned that role.

We completely disregard if there are any other roles that might be an admin role too (we could still mention it in the change record and/or release notes, and state people are free to do with that as they please; if there is no particular reason to have multiple admin roles, it would be recommended to remove any left-over admin roles. I am actually unclear why that is even possible, which probably led me to erroneously it is a single system setting). [Edit: actually, this does increase the scope of the unsupported scenario into sites that do have an "is admin" role, but it isn't the role named administrator but also have a non-admin role that is named administrator.]

The backup scenario ("scenario 4") would be unaltered.

If we ditch the admin role setting (which I agree is the cleanest), we will need alternative instructions to recover from scenario 4. I think such instructions would involve drush or drupal console in order to make the administrator role the admin role. Are we OK with that?

Alternatively, we could completely forget about supporting scenario 4 and write a hook_requirement that prevents people from updating in that situation:

  if ($phase === 'update') {
    // Check if there is an administrator role that is *not* configured to
    // be an admin role. This is a blocking situation that first needs to be
    // rectified.
    // @todo Remove in Drupal 10.0.0.
    $entityTypeManager = \Drupal::entityTypeManager();
    /** @var \Drupal\user\Entity\Role $role */
    $role = $entityTypeManager
      ->getStorage('user_role')
      ->load('administrator');

    if (is_null($role)) {
      // All good, we'll create it in user_post_update_user_1_with_admin_role().
      // Actually, when we get here *after* that update hook was executed, that
      // would be a sign of trouble.
      return;
    }

    if (!$role->isAdmin()) {
      $requirements['user_one_missing_role'] = [
        'title' => t('Administrator role'),
        'value' => t('Incompatible'),
        'description' => t(
          'In order to update to Drupal 9.2.0, the site should either <em>not</em> have a role with the machine name <em>administrator</em> <strong>or</strong> that role should also be flagged to be an administrator role. You have a role <em>named</em> <em>administrator</em> that is not actually configured to be an administrator role. See the <a href=":change_record_url">change record about this change</a> for more information.',
          [':change_record_url' => Url::fromUri('https://www.drupal.org/node/2910500')]
        ),
        'severity' => REQUIREMENT_ERROR,
      ];
    }
  }

This would probably create a bigger support burden right now, but it would basically ensure there are no sites running Drupal 9.2+ deviating from the standard administrator role setup (which has been one aspect of scenario 4 I don't like one bit). It would also mean that folks are a code rollback away from actually having a user interface to sort out the situation.

kristiaanvandeneynde’s picture

The hook_requirement in #296 is actually a nice solution.

Not too sure we want to change scenario 1-3 again, but I can see benefits to both approaches. So I'm on the fence.

benjifisher’s picture

Issue tags: -Needs usability review

Usability review

We discussed this issue at the end of #3198188: Drupal Usability Meeting 2021-02-19 and the beginning of #3200598: Drupal Usability Meeting 2021-02-26.

If I understand correctly, the role in question is created by an update function. If so, then the update function should give a link to the change record (CR) if the role is created.

As for the name of the role, we do not mind that it is long and ugly, since we expect (maybe even encourage) the site admin to change it.

We do not like the current proposal, "Admin (auto-generated by Drupal 9.2.0)". It is debatable whether using "Drupal" is a problem. In most contexts, we avoid that. Part of the reason is that a site may be using a distribution, and "Drupal" may not even be recognized by the site admin. A more serious problem is the version number. If I upgrade my site from 9.1.4 to 9.2.3 or to 9.3.0, then it is inaccurate and confusing to say that the role was created by Drupal 9.2.0.

Since a long, ugly name is a feature rather than a bug, why not include the URL of the CR in the role name? I do not have the exact text that we discussed at the meeting. Maybe @daffie has it. It was something like

Admin (auto-generated: see https://www.drupal.org/node/2910500)

Cut out the middleman: give the link to the CR instead of expecting the site admin to search for the role name.

daffie’s picture

This isn't true if user 1 tries to run the update, is unable to because it no longer has admin permissions, and therefore doesn't get them.

With the current MR this is not a problem as Drupal\Core\Session\UserSession::hasPermission() and Drupal\user\Entity\User::hasPermission() now both have BC layer that when the user id is 1 it returns true for all permissions. Therefore user with id 1 can always run the updates. Why then should we need hook_requirements? Or am I missing something?

AaronMcHale’s picture

Re #298:

"Admin (auto-generated: see https://www.drupal.org/node/2910500)"

This roughly matches what we agreed during the meeting - I have a feeling the one we settled on did mention "Drupal" somewhere, but as Benji points out (and during the meeting) the issue of the "white labelled distribution" (as I call it) did some up, so I also agree with the text in #289.

I do wonder if it makes the most sense to introduce this change as part of a major update (10.0), even though there has been extensive effort put into avoiding breaking existing sites, it does seem like the kind of potentially breaking change that people expect from a major update. Ultimately it's a communication and expectation issue, we can make it as obvious as possible to site owners but people just expect potentially breaking changes during major updates and are prepared to deal with those, they may be less prepared to expect any kind of breaking change during a minor update.

kristiaanvandeneynde’s picture

@daffie in #299 the latest patches remove those checks. So as soon as you have the new code, the check is gone and you still need to run updates.

kristiaanvandeneynde’s picture

I do wonder if it makes the most sense to introduce this change as part of a major update (10.0), even though there has been extensive effort put into avoiding breaking existing sites, it does seem like the kind of potentially breaking change that people expect from a major update.

It already got pushed from D8 to D9. Let's not push it forward another two years please.

Nothing changes but the fact that people should avoid coding if ($uid === 1), which was always a code smell to begin with. Furthermore, incorrectly written tests will now properly go red instead of thinking everything is fine when things are in fact broken.

A few rare configurations might see a new role introduced, but in a non-breaking way. Aside from that scenario (aka #4), there is nothing you need to do either post-update. Only the case that gets the new role has the option but not obligation to do something.

AaronMcHale’s picture

Re #302: after reviewing the IS and proposed resolution again, as well as looking through the MR, that makes sense now and I no longer see much of a BC issue.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

zenimagine’s picture

I installed 2 servers with Drupal 9.1 and I updated to Drupal 9.2

I just noticed that there are problems with the modification of the administrator account "user 1".

In the list of users, the role of the admin account is "Administrator", but if I modify the account, only the "Authenticated user" box is checked. If I check the "Administrator" box and register the account, no changes are registered.

If I add user picture for admin, it doesn't work, when I register the account, the photo is not remembered.

The other accounts have no problems, only User 1's has these problems.

If I change the username of user 1, it is changed, but not the role and not the photo.

Really, I don't understand a thing. There is no error in the Drupal logs.

steinmb’s picture

@zenimagine thank you for reporting, however I belive that your problem is unrelated to what we are trying to tackle in here. Please open a new issue with your findings.

clayfreeman made their first commit to this issue’s fork.

clayfreeman’s picture

I've attempted to rebase on 9.3.x, but I don't have the ability to update the target branch of the merge request.

AaronMcHale’s picture

Have we considered preventing the admin role from being deleted?

Tried searching for issues to make this change but nothing came up, it is currently possible to delete the admin role, which is fine, if you're UID 1.

Sorry if this has already been discussed, I couldn't find mention of it in the issue summary and the discussion here is very long.

Thanks.

nevergone’s picture

What can be done to move this forward?

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

AaronMcHale’s picture

It was noted in #3258321-10: Cancel account button on user form triggers server-side validation. that there's an access check for UID 1 on the "Cancel account" button/link, that will also need to be addressed at as part of this issue.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

nevergone’s picture

Is this expected in Drupal 9.5.x?
How can we move this issue forward?

Eric_A’s picture

How can we move this issue forward?

I did not try reading 300+ comments so maybe the suggestions below have been discussed already.

1. Summarize, and if possible categorize, the "quite some special behavior for uid #1" in the Issue summary.
2. Split this up into separate issues. I did not try to review the 55 changes from the merge request, but I had a quick look and my first question was if for example some test improvements could possibly be done independently. That alone could possibly already remove a lot of "distractions".

rivimey’s picture

Personally, I feel this issue is too significant a change to go into a minor release, and it will need a shakedown. The best bet, therefore, is to get it into D10-beta release, if that can be done at this stage, and if necessary/appropriate then consider a backport to D9.

Either way, lets do *something*!!

samaphp’s picture

Why do we need to keep uid 1? please read

I will try to list some of the arguments for removing uid:
- "shoot yourself in the foot" by removing uid 1 accidentally. A: this can happen even if you are administrator and unassinging role from yourself.
- The Paranoia module will still allow uid 1 A: Simply the module can require blocking/deleting the uid 1
- harder to keep track of who knows the password to your super account than if you give everyone their own A: On production it is risky to have multiple users with roles that can change site configuration, delete views, or any change to the structure. We believe production should be a Business responsibility. We all know that `drush uli` can give you a on-time login URL of any user. so it is hard to keep track of who used that user anyways. The best is to block uid 1.

uid 1 should be always blocked on production!
in case of uid 1 is not exists, this will be a huge change, how can the admin user get the full permission of the installed module? in case of "administrator" role is not exists? or he will need to assign the permission manually after every new module installed.
It is also for security reason once we block the uid 1 we are pretty sure no one can assign a role if there are any permission to manage people.

In minimal installation there are no administrator which is a good for us as developers to make sure no one on all other environments will have the permission to make changes to site configuration like creating or modifying views for example. This will make the Drupal config-sync useless if we allow doing some changes on production. Instead, we are doing it through git and importing the configuration.
We found the best practice is to have the website with only business user roles only (no administrator role by using the minimal profile). No roles for developers except the uid 1 for emergency cases only, and it is always blocked on production.
And on local machine developers are using the uid 1 user for development works.
We believe this increases the data integrity and makes sure no one can break the site structure while we manage the changes using Drupal config-sync.

So, I think uid 1 is critical to keep the user roles list having a basic roles for production unless the uid 1 decided to create a role "administrator" and give him the full permissions.

If the community decided to remove uid 1 we will lose the super user 1 power and we will have to add (new role with full permission) instead we have only uid 1 to get all permissions. So, please anyone facing an issue with user 1 can delete it or block it. But for us who rely on uid 1 we will have to do a New role and too much-assigned permission. and every new module installed we will need to add permission to this role.

Thanks.

kristiaanvandeneynde’s picture

Not sure if #317 read up on the previous comments, because most of what they are arguing has been covered already:

  1. The system will have an admin role that cannot be deleted
  2. Said admin role can be assigned to anyone, not just UID 1
  3. An upgrade path would need to figure out whether to grant UID 1 said admin role

Also this:

If the community decided to remove uid 1 we will lose the super user 1 power

Should really translate into this:

If the community decided to remove uid 1 hackers will no longer have an easy god mode account to target


I've put a lot of work into this *checks notes* five years ago and it basically got stalled on the fact that a large amount of tests (and even some code) relies on UID1. At some point, I simply gave up, but the goal remains one we should strive for.

God mode accounts have no place in any system that wants to call itself secure and we are still facing code smells in some core subsystems and a large amount of tests because of it.

AaronMcHale’s picture

I've put a lot of work into this *checks notes* five years ago and it basically got stalled on the fact that a large amount of tests (and even some code) relies on UID1. At some point, I simply gave up, but the goal remains one we should strive for.

I wonder if it makes sense then to split that - (if you like) ground work - off into one or more separate issues. That might make this more manageable and easier to get done.

rivimey’s picture

A possible way forward would be to issue a number of changes that:

  • One or more changes to the code that explicitly check for uid==1 to do something else, possibly a new is_superuser() function.
  • One or more tests that override is_superuser() with some other check and then run core tests, to discover what has been missed
  • Make a change to core such that is_superuser() becomes the only valid way to check (not sure how), and issue that as an official change. Maybe this could be the superuser's uid becomes some other value?
  • ... watch the contrib complaints come in...
  • ... revisit the rest of the changes in this module, to make the new role and use it.

I still feel that the major change here needs to be done on a major Drupal version change, i.e. 9 to 10 or 10 to 11, so this issue is rapidly running out of time to be applied in the next year or so.

AaronMcHale’s picture

... possibly a new is_superuser() function ...

Sounds like a good candidate for a follow-up issue, it could be useful to have a simple method on the UserInterface which allows checking if the user is an admin user.

Such a function would essentially abstract away this logic (example from the merge request):

$rids = $user->getRoles();
$roles = Role::loadMultiple($rids);
foreach ($roles as $role) {
  if ($role->isAdmin()) {
    return TRUE;
  }
}

Right now, each role has that flag is set individually, so it's theoretically possible for more than one role to be "the admin role". #3229029: Introduce a role.settings config object and move 'Administrator role' out of the Role entity config will change that and we might also have an opportunity there to do something slight more elegant. Either way, it might be worth opening a dedicated follow-up issue to look at this, since this issue is already quite large.

I still feel that the major change here needs to be done on a major Drupal version change, i.e. 9 to 10 or 10 to 11, so this issue is rapidly running out of time to be applied in the next year or so.

I think as long as we have a reliable upgrade path I don't see a reason this can't be done in a minor version. The merge request essentially already has this, it ensures that UID 1 has the admin role, thereby not breaking any existing sites.

catch’s picture

Version: 9.5.x-dev » 10.1.x-dev

A lot of the test changes here could be done outside of this issue - I put a couple of comments in the MR on one that could happen before this lands, and one that could be done in a follow-up. That would definitely help to make the MR more reviewable, reduce conflicts with other issues etc.

@rivimey we don't commit large refactorings to major releases any more (since 8.0.0 was released), everything goes into a minor release with a bc layer, and then the bc layer is removed in the next major release. Sites have more to deal with getting from major release to major release (platform requirements, module compatibility), and any kind of complex data upgrade path or new deprecations make this harder.

For a potentially disruptive change like this, we would try to commit it towards the beginning rather than the end of a minor development cycle, this still gives contrib months to adapt any tests or similar. Right now that would be 10.1.x.

rkoller’s picture

Version: 10.1.x-dev » 9.5.x-dev

Also a +1 for moving this issue forward.

And I wanted to bring #3293874: Prevent accidental lockouts on the Role settings page and clarify the corresponding description for its select box onto the radar. The issue is already a potential problem but as soon as the UID#1 gets removed things get even more problematic and challenging for users to fix an accidental change of the role settings as described in the issue summary. Then there is also #3229029: Introduce a role.settings config object and move 'Administrator role' out of the Role entity config which reduces the number of administrator roles to a single one as far as I understand. With the removal of UID#1 and the reduction to a single administrator role solving #3293874: Prevent accidental lockouts on the Role settings page and clarify the corresponding description for its select box gets even more mandatory imho since there isn't any fallback left enabling the user to fix any accidental misconfigurations on the role settings page.

rkoller’s picture

Version: 9.5.x-dev » 10.1.x-dev

huh accidentally the version was changed as well on my comment. sorry for that. reverting back to 10.1.x

rivimey’s picture

Such a function would essentially abstract away this logic (example from the merge request):

Yes, definitely. I would consider the snippet quoted an implementation detail when the actual requirement was 'does this context have admin privilege?'. Note I said context... could admin privilege be associated with a session but not a specific user, e.g. because of a specific IP address? I mention this not because I have a use-case but because it is IMO best to avoid making assumtions.

@rivimey we don't commit large refactorings to major releases any more

Thanks @catch. I understand, though I admit I'm nervous about this one. I know I tend to apply minor releases to core with less testing (it is semver after all), and wouldn't be surprised if that was common.

AaronMcHale’s picture

I would consider the snippet quoted an implementation detail when the actual requirement was 'does this context have admin privilege?'. Note I said context... could admin privilege be associated with a session but not a specific user, e.g. because of a specific IP address? I mention this not because I have a use-case but because it is IMO best to avoid making assumtions.

Yeah I could see a potential use-case for something like that, maybe someone creates a contrib module to allow that which might be useful for local development (or maybe one already exists?). Probably best to open a follow-up issue so these points can be properly explored.

rkoller’s picture

Balu Ertl’s picture

Title: Remove the special behavior of uid #1. » Remove the special behavior of UID#1

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

kristiaanvandeneynde’s picture

Just mentioning that this could finally be considered fixed if/when #3371246: [Meta, Plan] Pitch-Burgh: Policy based access in core lands.

At that point, UID 1 being special is just an access policy like any other in the system and you could choose to turn this off on a per-site basis. So we wouldn't be shoving this change down everyone's throats, but could rather document and recommend how to turn this off on production sites.

The only thing we would have to do to make that fully possible is find all of the places in core where a UID 1 check happens and update them to properly check for a permission instead. That effort is already taken care of in part by the patches here, so looks like we could finally land this issue if/when we get access policies in core.

kristiaanvandeneynde’s picture

More specifically here is how we could fix this issue:

  1. Commit access policies into core, turning UID1 in an access policy
  2. Make all tests run with the UID 1 access policy service turned off so that we are sure we test the right permissions
  3. Update code where UID 1 is still being checked and turn into permission check
  4. Once all of core tests go green with UID 1 turned off, close this issue
  5. Write documentation on how to turn off UID 1 on your (production) site and recommend you do so

Doing all of the above means we get to keep UID 1 for those who are used to relying on it, but also that those who want a more secure website can turn off UID 1's god mode and we can guarantee that all of core works with UID 1 turned off.

solideogloria’s picture

Doing all of the above means we get to keep UID 1 for those who are used to relying on it

No, it means that Drupal's contributors and security team now need to support two different admin situations, with one of them being less secure. UID 1 is not something that should remain an admin account option. If someone wants the same functionality, they can create a user with UID 1 if one doesn't exist, and assign it the new admin permission/role.

kristiaanvandeneynde’s picture

No, it means that Drupal's contributors and security team now need to support two different admin situations

That's simply not true. With how the new access policy system works, we'd simply support permission checks, no matter how many or few permissions you have. By turning off the UID 1 access policy during tests, we make sure people don't accidentally think their module is safe when in fact UID 1 was giving false results.

Given how much backlash there has already been in this thread, I'd say what I suggested in #331 is a nice first step. It addresses the concerns of people in this thread regarding being locked out of their site (turn on the service again and you're in) and keeps the old behavior OOTB. But, you'd be able to turn off UID 1's god mode without having to patch or break core.

In a later release we could still discuss removing the service altogether, but for now I think this would be a nice way to gradually move away from old habits.

rivimey’s picture

Re #333 I agree. It sounds like a good plan, especially as those who care can reinstate the old behaviour in a forward-compatible way.

Balu Ertl’s picture

mxr576’s picture

kristiaanvandeneynde changed the visibility of the branch 10.3.x to hidden.

kristiaanvandeneynde changed the visibility of the branch 11.x to hidden.

kristiaanvandeneynde changed the visibility of the branch 9.2.x to hidden.

kristiaanvandeneynde changed the visibility of the branch 9.3.x to hidden.

kristiaanvandeneynde’s picture

Assigned: Unassigned » kristiaanvandeneynde
Issue tags: -Needs screenshots

Right let's give this a spin then. Started a new branch and MR because of how old the other one was and we have a different starting point now: the SuperUserAccessPolicy, which landed in core yesterday.

kristiaanvandeneynde’s picture

Seems like it's not properly removing the service yet...

kristiaanvandeneynde’s picture

Right, got it to work for Kernel tests. Now need to test locally if it also works for browser tests.

kristiaanvandeneynde’s picture

Okay this is awesome.

We can now simply flag the failing tests rather than having to fix them here. This allows us to fix them in individual (perhaps Novice-tagged) follow-ups. It's crazy how small this MR is for what it's doing and that all thanks to the new Access Policy API.

kristiaanvandeneynde’s picture

Unit tests are failing but there is no reported failure in the log o.O

kristiaanvandeneynde’s picture

Okay if I add $this->assertFalse($this->rootUser->hasPermission('foo')); to a browser test it correctly finds that UID 1 does not have all permissions. But I've got a feeling that the actual pages we're visiting still have UID 1 as god mode.

I expected some failures and am seeing none, that doesn't seem right.

kristiaanvandeneynde’s picture

Yeah, this leads to a status code of 200 in the second assertion:

  public function testSuperUser() {
    $this->drupalget('admin');
    $this->assertSession()->statusCodeEquals(403);
    $this->drupalLogin($this->rootUser);
    $this->drupalget('admin');
    $this->assertSession()->statusCodeEquals(403);
  }

Seems like I need to figure out how to persist the container parameter.

kristiaanvandeneynde’s picture

Assigned: kristiaanvandeneynde » Unassigned

Still working on figuring the browser tests out but unassigning so someone else can have a go if they better understand what's going on

kristiaanvandeneynde’s picture

Status: Needs work » Needs review

Never mind, somewhere rootUser is getting the administrator role. Turns out all of our browser tests are already super-user-safe in a sense that they do not test with UID 1 by accident. They do, however, test with an admin account sometimes but that's totally fine.

Tried adding the following crappy test and it went green:

  public function testSuperUser() {
    $this->drupalget('admin');
    $this->assertSession()->statusCodeEquals(403);

    $user = User::load(1);
    $user->removeRole('administrator');
    $user->save();

    $this->drupalLogin($this->rootUser);
    $this->drupalget('admin');
    $this->assertSession()->statusCodeEquals(403);
  }

So that means this MR takes care of browser tests and kernel tests 🎉

kristiaanvandeneynde’s picture

Issue summary: View changes
Issue tags: +Needs followup

Updated the issue summary as I feel this issue can now be considered "done". Provided we figure out why the unit tests are failing seemingly for no reason. Tagging as needs followup for all the tests that need refactoring and the recovery.php script.

kristiaanvandeneynde’s picture

The follow-up should adjust hook_file_url_alter() to no longer mention UID1 and replace MigrateAccessCheck. IsSuperUserCacheContext was obviously going to be deleted in the follow-up and #3436395: UserRolesCacheContext can lead to poisoned cache returns for user 1 takes care of the UserRolesCacheContext.

I found no other mentions of UID 1 during a brief look, but it's up to the followup issue to be thorough on that.

kristiaanvandeneynde’s picture

Aha, now we're seeing progress. The browser test fails I was expecting are coming through in droves

smustgrave’s picture

Status: Needs review » Needs work

Seems to be feedback. But confused on the follow up to fix tests? Feel they would have to be fixed here to be merged.

kristiaanvandeneynde’s picture

So currently these tests only go green because they accidentally test with user 1. The changes I made makes these tests go red, but if you add the lines:

  /**
   * {@inheritdoc}
   *
   * @todo Remove and fix test to not rely on super user.
   */
  protected $usesSuperUserAccessPolicy = TRUE;

The super user access policy is turned on for that test again, making it go green once again.

This enables us to commit the changes here without bloating the MR with dozens of test changes. It's already at 40 files changed and I have yet to change even more Functional tests (but ran out of time Friday evening). I'll get to that on Tuesday probably or later this week.

Then, we indeed need a meta issue to track all of the needed changes to these tests, probably one issue per test (hopefully tagged as Novice).

smustgrave’s picture

Then probably would need it’s own feature branch, don’t know the policy around those. But can’t imagine we can merge to 11.x with test failures.

quietone’s picture

@smustgrave, it is a side effect of removing the special privileges of user 1 that tests are identified that need those privileges. That is a good thing and adding protected $usesSuperUserAccessPolicy = TRUE just allows the test to pass using the same privileges it had before this change. So, in effect is not changing the test. It ran with elevated privileges before and this MR is continuing that. However, we want to check these tests and see if they can be modified to run without special privileges. And this last part can be done in a followup task. I hope that helps.

smustgrave’s picture

If the follow ups are novice level I've seen a few issues getting tagged for GreeceSpringSprint2024, may be a good opportunity.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review

There are 8 files actually being changed, meaning we have found 71 broken tests so far. That's going to be a tall followup list in the meta issue.

kristiaanvandeneynde’s picture

Think the nightwatch tests are failing because of this:

exports.command = function drupalInstallModule(module, force, callback) {
  const self = this;
  this.drupalLoginAsAdmin(() => {
    this.drupalRelativeURL('/admin/modules')

I've never worked with Nightwatch tests before so trying to figure out how to get to the PHP version of drupalLoginAsAdmin

kristiaanvandeneynde’s picture

Okay, found the culprit:

exports.command = function drupalLoginAsAdmin(callback) {
  const self = this;
  this.drupalUserIsLoggedIn((sessionExists) => {
    if (sessionExists) {
      this.drupalLogout();
    }
    const userLink = execSync(
      commandAsWebserver(
        `php ./scripts/test-site.php user-login 1 --site-path ${this.globals.drupalSitePath}`,
      ),
    );

Ends up calling TestSiteUserLoginCommand, which is intended for logging in a user, not logging in an admin user. So this part of every Nighwatch test is relying on UID 1 being an admin, which it no longer is unless TestSiteApplication uses the standard or umami_demo profile, where user 1 is assigned the admin role.

So let's see if we can either assign UID 1 the admin role or rethink whether we should use drupalLoginAsAdmin() at all. Perhaps that rethinking can be done in a follow-up.

kristiaanvandeneynde’s picture

Status: Needs review » Needs work

Hmm, so I figured out where Nightwatch is going wrong but I'm not sure I can fix it. I tried adding an extra argument to TestSiteUserLoginCommand as a stopgap, but that's not being picked up it seems.

kristiaanvandeneynde’s picture

Status: Needs work » Needs review

All green now, will leave the Nightwatch stuff to the JavaScript experts in a dedicated followup.

kristiaanvandeneynde’s picture

Issue summary: View changes

Followup created and added as an @see to all @todos: #3437620: [Meta] Fix all tests that rely on UID1's super user behavior

kristiaanvandeneynde’s picture

Issue tags: -Needs followup

Updated the CR: https://www.drupal.org/node/2910500

As far as I am concerned this issue is now "done". It:

  • Allows people to turn off the super user on their website
  • Turns off the super user for all browser, kernel and nightwatch tests unless opted out
  • Opts out all failing core tests, so they can be taken care of in the followup meta issue

The simplicity of turning off UID1 now compared to the old patches/MR is a testament to how powerful the new Access Policy API is.

kristiaanvandeneynde changed the visibility of the branch 540008-remove-the-special to hidden.

smustgrave’s picture

Status: Needs review » Needs work
kristiaanvandeneynde’s picture

Status: Needs work » Needs review

Re #367 I actually did that to get the list of tests for the meta issue but forgot to commit :)

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Changes seem good. For the META talking an event organizer for a camp in Greece so maybe a few of them can get knocked out.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

I don't think we should be changing the behaviour in contrib and custom tests without going through some sort of deprecation process. This change could break an organisations test suite and cost them work.

We should default the FunctionalTestSetupTrait $usesSuperUserAccessPolicy property to NULL and then do something like:

      if ($this->usesSuperUserAccessPolicy === NULL) {
        $test_file_name = (new \ReflectionClass($this))->getFileName();
        // @todo Decide in https://www.drupal.org/project/drupal/issues/TO_OPEN when/how to trigger deprecation errors or even failures for contrib modules.
        $this->usesSuperUserAccessPolicy = str_starts_with($test_file_name, $this->root . DIRECTORY_SEPARATOR . 'core');
      }

Just before it is used in the trait. This will allow contrib and core tests to adopt is a needed.

kristiaanvandeneynde’s picture

kristiaanvandeneynde’s picture

Status: Needs review » Reviewed & tested by the community

Implemented suggestion verbatim bar the negation, which was needed. Tests go green on CI, confirmed that core tests run correctly locally. So back to RTBC I suppose.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

I think the code is good to go now... but the CR just needs a few improvements and we need
The CR needs to be updated the explain the test situation. I.e. non core tests still have the super user policy enabled and uid 1 is special. Core tests do not. And it needs to explain the flag and how to set it false if you don;t want your tests to rely on uid 1 specialness.

Please we need to create follow-up to:

  1. issue a deprecation if the test flag is set to anything but TRUE... I think the deprecation should be for Drupal 12.
  2. Discuss recovery steps eg. the document page or recovery.php idea (I'm deeply suspicious of the recovery.php idea because that's a recipe for an attack target)

Can set back to rtbc once that's done.

kristiaanvandeneynde’s picture

Status: Needs work » Reviewed & tested by the community

Updated the CR and created a few new issues. All of them are now grouped under the following meta issue: #3438901: [Meta] Plan for deprecating and eventually removing the super user access policy

alexpott’s picture

Adding a release note. Still need to sort out issue credit... that might take a while.

kristiaanvandeneynde’s picture

Hahaha I can imagine. 15 years and 375 comments later...

alexpott’s picture

Updating issue credit. I've tried to credit everyone who has made constructive contributions to the issue including disagreeing to previous patches. Obviously this is subjective so if I've made a mistake please let me know.

alexpott’s picture

Title: Remove the special behavior of UID#1 » Add a container parameter that can remove the special behavior of UID#1
Issue summary: View changes

Re-titling so that the issue title matches what is happening here and updating issue summary to be correct.

alexpott’s picture

Version: 11.x-dev » 10.3.x-dev
Status: Reviewed & tested by the community » Fixed

Committed and pushed 9baa43976c to 11.x and 97c8bdc7ec to 10.3.x. Thanks!

  • alexpott committed 97c8bdc7 on 10.3.x
    Issue #540008 by kristiaanvandeneynde, Spokje, daffie, clayfreeman,...

  • alexpott committed 9baa4397 on 11.x
    Issue #540008 by kristiaanvandeneynde, Spokje, daffie, clayfreeman,...
kristiaanvandeneynde’s picture

Awesome, thanks! 🎉

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.