Convert permission providers to tagged services; unify with a service locator

Created on 15 February 2024, 11 months ago

Problem/Motivation

Continuing the trend ( 1 πŸ“Œ Convert StreamWrapperManager to use a service locator Needs review , 2 πŸ“Œ Convert CheckProvider to use a service locator Fixed , 3 πŸ“Œ Convert QueueFactory to use a service locator Fixed , 4 πŸ“Œ Convert CacheFactory to use a service locator Needs work ) of adding more service locators in core. This issue resolves to convert user permission callbacks as defined in *.permissions.yml files to anonymous services, bundled together in a service locator, injected to a dedicated locator service, which is then used by PermissionHandler to build a list of available permissions.

The result, will be that devs can create permission providers in the same manner as existing, but with proper (tagged) services, not a bespoke definition in a YAML file. In turn, since these become real services, we'll be able to use Attributes and Autoconfig[er/u]ration.

The ultimate goal (out of scope) would be to simply create a file in a module, add an attribute to a method, without the need for a services.yml entry. However we are a couple of steps away from that. So here are the building blocks for permissions as tagged services.

Future

After this issue, we can look to implement a namespace + attribute so we dont need to manually define permission providers in either a services.yml/permissions.yml. It'll get discovered automatically, for example:

namespace Drupal\my_module\PermissionProvider;

class MyPermissionProvider {
	
	#[AsPermissionProvider]
	public function permissions(): array {}
}

Proposed resolution

  • Allow permission providers to be defined as tagged services.
  • Promote existing permission providers from permissions.yml files to services
  • Implement a service locator for the tagged services.
  • Utilise the service locator in PermissionHandler

Remaining tasks

Review, Merge.

User interface changes

None.

API changes

  1. Existing permission providers are converted to anonymous services (unusable outside of the locator)
  2. Arguments for user.permissions modified.
  3. Deprecation message for extenders of \Drupal\user\PermissionHandler
  4. New service locator service, accessible by autowiring.

Data model changes

Nil.

Release notes snippet

Permission providers may now be implemented as tagged services instead of callbacks in module.permissions.yml files.

πŸ“Œ Task
Status

Active

Version

11.0 πŸ”₯

Component
User systemΒ  β†’

Last updated 3 days ago

Created by

πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @dpi
  • Status changed to Needs review 11 months ago
  • πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia

    Implementation (MR) notes

    The implementation here follows one I am familiar in with Messenger; classes are converted to anonymous tagged services, and grouped together with SendersLocator, which is then injected elsewhere.

    The mapping between a method and a service is stored in a mapping array, and passed to the locator service so it knows which method to invoke on the service in locator. This mapping strategy is traditionally the way of storing in the locator which method is associated with a method tagged with a PHP attribute.

    Services are created using the anonymous ID with hash + random strategy.

    YAML discovery in PermissionHandler is still used for statically defined permissions. permission_callbacks is now completely ignored.

    PermissionHandler has been switched to use autowiring. I think we can modify the services.yml definition at will, while the class constructor itself is able to handle deprecations (?)

    Extensive use of PHPStan array shapes to improve static analysis of arrays.

    Each way of implementing a permission provider, has been added to the new user_permission_provider_test.module.

    The existing way of working exists, including compatibility with ControllerResolver/ContainerInjectionInterface.

    If there is an existing service with the same ID (via autowiring) that class, it will be used instead of creating a new service.

    The PermissionHandler unit test (PermissionHandlerTest) has a few YAML mocks removed, in favor of the new Locator with mock data. There is still a YAML test case there for necessary coverage. This unit test file has also been cleaned up a bit with redundant property assigns removed.

    The permission provider locator (PermissionProvidersLocator) intentionally does not implement an interface, and the service is private. I don't see a need for it, and it could be added (and un-privatized) at a later date.

  • Pipeline finished with Success
    11 months ago
    Total: 472s
    #95724
  • πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia

    Created a change record: https://www.drupal.org/project/drupal/issues/3421573 πŸ“Œ Convert permission providers to tagged services; unify with a service locator Needs review

  • Pipeline finished with Canceled
    11 months ago
    Total: 54s
    #95753
  • Pipeline finished with Success
    11 months ago
    Total: 480s
    #95756
  • Pipeline finished with Success
    11 months ago
    Total: 482s
    #95769
  • Issue was unassigned.
  • πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia

    Updated MR with feedback.

  • Pipeline finished with Success
    10 months ago
    Total: 523s
    #96438
  • πŸ‡¦πŸ‡ΊAustralia kim.pepper πŸ„β€β™‚οΈπŸ‡¦πŸ‡ΊSydney, Australia

    This issue might conflict πŸ“Œ Static cache permissions Needs work

  • πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia

    Happy to deal with #2339487 whether it or this issue gets in before. Fortunately theres no dependency/blocker between these issues.

    I've just added a review to that issues MR @ !6635.

  • πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia

    ✨ Directory based automatic service creation Needs review will fulfill the future possibilities for automatic permission provider registration.

  • πŸ‡§πŸ‡ͺBelgium kristiaanvandeneynde Antwerp, Belgium

    Okay so after skimming this:

    1. I like the idea
    2. I like the use of modern php stuff, but am hesitant to see functions yield arrays with multiple pieces of information.
    3. I'm not sure why you would use tagged services and then still allow people to specify callbacks in their permissions.yml files. To me it feels like we should use one or the other, not both. I'm personally in favor of tagged services.
    4. For the time being I would allow and process both ways for BC reasons but throw a deprecation warning when callbacks are still detected in permissions.yml files.
    5. Disclaimer: I haven't checked ✨ Directory based automatic service creation Needs review nor the 4 issues linked in the IS yet and maybe that might change some of the above points.

    All in all I really appreciate you working on this as I fully agree what we currently have isn't modern at all. Listing callbacks inside yaml files feels a bit outdated.

  • Assigned to dpi
  • Status changed to Needs work 10 months ago
  • πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia

    Thanks for the feedback, I'll tweak the locator a little and deprecate callbacks in YAML.

  • Pipeline finished with Success
    10 months ago
    Total: 471s
    #103341
  • Pipeline finished with Canceled
    10 months ago
    Total: 301s
    #103367
  • Pipeline finished with Failed
    10 months ago
    Total: 459s
    #103369
  • Pipeline finished with Failed
    10 months ago
    Total: 188s
    #103377
  • Pipeline finished with Failed
    10 months ago
    Total: 549s
    #103382
  • Pipeline finished with Success
    10 months ago
    Total: 993s
    #103388
  • Status changed to Needs review 10 months ago
  • πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia

    I've refactored the permission locator slightly, per Kristiaan's feedback.

    I've also deprecated permission_callbacks in YAML as suggested by Kristiaan. This change increases the LOC's significantly, but removes quite a lot of redundant lines in permission classes.

    Changelog has been updated with deprecation wording, plus notes on deprecation scope β€” https://www.drupal.org/node/3421580 β†’

    Tests back to green.

  • Issue was unassigned.
  • πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia
  • Status changed to Needs work 10 months ago
  • The Needs Review Queue Bot β†’ tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

    This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

    Consult the Drupal Contributor Guide β†’ to find step-by-step guides for working with issues.

  • Pipeline finished with Failed
    10 months ago
    Total: 163s
    #112786
  • Pipeline finished with Success
    10 months ago
    Total: 482s
    #112790
  • Status changed to Needs review 10 months ago
  • πŸ‡¦πŸ‡ΊAustralia dpi Perth, Australia

    Fixed conflicts after πŸ“Œ Deprecate ModuleHandlerInterface::getName() Fixed .

    Still needs review.

  • πŸ‡¬πŸ‡§United Kingdom longwave UK

    Why do we need a service locator here? We always need all tagged services, we don't need to refer to them individually for any reason, we just loop over the whole set - the service collector pattern seems to fit better? We could just add each permissions provider directly to the PermissionHandler.

    Also, we shouldn't need to specify the "provider" tag manually, that should somehow be automatically inferred.

  • πŸ‡«πŸ‡·France andypost

    Is there a way to populate provider at discovery level instead of duplicating the module name?

  • Status changed to Needs work 10 months ago
  • The Needs Review Queue Bot β†’ tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

    This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

    Consult the Drupal Contributor Guide β†’ to find step-by-step guides for working with issues.

  • πŸ‡΅πŸ‡°Pakistan usmanjutt84 Islamabad

    Updated the "Change record" URL.

  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    I unpublished the change record, pretty sure it's not ready.

  • πŸ‡΅πŸ‡°Pakistan usmanjutt84 Islamabad

    I agree. Thank you @nicxvan.

    I appreciate the idea of generating dynamic permissions using services and tags, rather than relying on permission callbacks. The current implementation in the GraphQL 4 module utilizes the service name as a permission callback, which leads to errors on installin the module via drush because the service is not be registered at the time permissions are being generated. By adopting a service-based approach, this issue would be resolved in future releases, ensuring smoother functionality and enhanced reliability. See the issue service class does not exist on installing πŸ› service class does not exist on installing Active .

  • πŸ‡§πŸ‡ͺBelgium kristiaanvandeneynde Antwerp, Belgium

    I just read @longwave's comment in #15 and I fully agree we don't need a locator here. A simple service collector (tagged services) would be better here. Repeating what he said:

    • When we need the permission providers, we need all of them. So no point in having the option to lazy load them.
    • When we want to build a permission UI or verify permissions on save, only then will we need the collector, so not using a locator has no drawbacks here

    Furthermore I agree with the following:

    I am also still weak -1 on having to specify the method, it feels like permissions services should just have a fixed interface

    If a module for some reason needs multiple ways to provide permissions, it should simply provide multiple services; not multiple methods on a single service. Heavy +1 for a simple interface.

  • πŸ‡¬πŸ‡§United Kingdom joachim

    I think this is a change in the wrong direction. Declaring dynamic service provider classes in the same location as the fixed services is good DX.

    This is a pattern we should be extending -- #2910814: deprecate magic ServiceProvider file discovery; declare in services.yml β†’ -- not removing.

  • πŸ‡§πŸ‡ͺBelgium kristiaanvandeneynde Antwerp, Belgium

    @joachim Are you alluding to the IS (which might need updating)? My last comment basically steers the MR towards tagged services, which are declared in the services.yml file. And that seems to be exactly what you want to see more of, or am I misreading this?

  • πŸ‡¬πŸ‡§United Kingdom joachim

    Argh I mistyped, sorry. I meant to say:

    Declaring dynamic permissions provider classes in the same location as the fixed permissions is good DX.

    So in other words, I think a dynamic service provider class should be declared in services.yml, and a dynamic permissions provider class should be remain as being declared in permissions.yml.

    If you're looking for services or permissions, those files are where you go to look.

  • πŸ‡§πŸ‡ͺBelgium kristiaanvandeneynde Antwerp, Belgium

    Normally I would agree, but as the IS states we're specifically looking to turning these providers into services so that we can benefit from everything the container has to offer us. If we were to keep them inside the permissions.yml file, we are severely limited as to what we can do.

    One could even argue the permissions.yml file is another "magic file in a magic place" like the MyModuleServiceProvider class. In my opinion, the more callable code we can put into the container, the less custom code we have to write for these types of discoveries. The current controller approach means we need to inject the container to fetch said controller's dependencies, an approach that is not encouraged by Symfony (see removal of ContainerAwareTrait).

    The way I see it:

    • Permissions can only come from tagged services
    • Core ships with one generic such tagged service that scours every module folder for a .permissions.yml file and gathers those
    • Each module can then add as many such tagged services as they like, although usually one should suffice

    Upside of this approach is that you can now turn off a service you don't want permissions from or decorate it to alter the result. With the current approach that's simply not possible.

Production build 0.71.5 2024