Access policies should be marked as final to encourage decorating

Created on 29 May 2024, 7 months ago
Updated 17 June 2024, 6 months ago

Problem/Motivation

I've started porting Group's old policies to the new Access Policy API and realized that it would be a BC break to make them use the new core interfaces because someone might have extended my policies' classes and swapped them out in the service definitions. So I would be breaking their code and therefore have come up with an alternative (temporary) solution before I tag a new major release where I switch to core's API.

However, this has got me thinking. Extending an access policy, which is a tagged service, is actually something we should discourage, if not make impossible. We've had the issue before with the entity type handlers where only one module could ever swap out a handler because they were essentially all extending the same class and changing the same plugin definition entry. This made it so it was really hard if not impossible for multiple entity access modules to work together.

Group has fixed this problem by using a services-based handler system that was built with decorating in mind. Given how small the interface for an access policy is, I would also advocate for us changing the two access policy classes in core to final before we release it with 10.3. Or at the very least make them final in 11.0.

Keep in mind, you should never swap out an access policy with an extended class in contrib code because then we go back to the dark ages of incompatible access modules. Now is the chance to create a better ecosystem because the API is new. So contrib will always have to opt for decorators, or access policies of their own that make good use of the build and alter phase.

Project/site code, can still swap out the original class by decorating it with a high priority and then not calling the parent. So that avenue is still available when marking access policies as final, it's just far more clear from the code that this is not encouraged.

There was also a discussion on Slack about this. There were +1s from catch and cmlara and some pushback from neclimdul, but I think I that was properly addressed when I stated there should never be a case where you want to extend an existing policy as the base of a new policy.

After all, that makes no sense from the API's point of view because you'd be returning a large overlap with an existing policy and the end result would get merged anyway, leading to a waste of computing resources. On top of that, cmlara correctly pointed out that a base class is provided and all new access policies should want to extend that one instead. So while we're not advocating for using final all over core, for this API specifically it makes a lot of sense.

I intend to go into much more detail on this subject at DrupalCon Barcelona, but given the time issue I feel like we should probably tackle this before D10.3 or, less preferably, D11.

Steps to reproduce

N/A

Proposed resolution

Mark access policies as final and document this so contrib follows suit.

Remaining tasks

Mark the two policies in core as final.

User interface changes

N/A

API changes

Two policies in core become final.

Data model changes

N/A

Release notes snippet

N/A

📌 Task
Status

Fixed

Version

10.3

Component
Base 

Last updated 1 day ago

Created by

🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

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

Merge Requests

Comments & Activities

  • Issue created by @kristiaanvandeneynde
  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium
  • 🇺🇸United States moshe weitzman Boston, MA

    I dont feel too strongly about this. The OP makes some good arguments for this. Some counter-arguments:

    • Its a bit unrealistic to have multiple access policies magically work together. Each site would have different requirements about what happens when the policies overlap
    • Is decorating a class really much safer than extending it. You still "depend" on the inner class in a significant way.
    • I'm from the "we're all consenting adults" philosophy. I would slap @internal on the policy classes and then any developer who chooses to extend does so at own risk.
  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

    Replying to #3:

    Each site would have different requirements about what happens when the policies overlap

    Emphasis mine. I am mainly worried about contrib. If two modules want to drastically alter another module's or core's access policy in a way that they can't do from their own policy's calculatePermissions() or alterPermissions() then they would both end up extending the contrib/core policy, making them incompatible with the other module.

    If they both were to decorate said access policy, then both decorators would fire and you might get lucky. But you'll never get lucky if we allow extending/swapping over decorating.

    Is decorating a class really much safer than extending it.

    It's not about safety or BC, that's merely what got me thinking. It's about interoperability, something which is completely impossible when using extensions. If multiple modules want to slightly affect an existing third-party policy, they can without breaking the others.

    I would slap @internal on the policy classes and then any developer who chooses to extend does so at own risk.

    But that's the thing, while they are largely internal and you should be able to get the job done with a custom policy's alterPermissions() there might be valid edge cases where you still want to alter the original class. If we mark it as @internal, that might leads to people being hesitant about decorating it, even though we definitely want to encourage decorating.

  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

    TL;DR: I would like to see an ecosystem where interoperability isn't necessarily guaranteed, but it's highly likely. By allowing extensions, core is shooting itself in the foot because as soon as one module chooses to extend over decorating, the gate is immediately closed for all other modules to do the same.

  • Merge request !8214Mark core access policies as final → (Closed) created by kristiaanvandeneynde
  • Status changed to Needs review 7 months ago
  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium
  • Pipeline finished with Failed
    7 months ago
    Total: 129s
    #185038
  • Pipeline finished with Failed
    7 months ago
    Total: 120s
    #185037
  • Pipeline finished with Success
    7 months ago
    Total: 637s
    #185040
  • 🇺🇸United States smustgrave

    Can the MR be updated to point to 11.x or a 2nd one opened up? SInce both 10.3 and 11.0 are in beta.

  • Merge request !8228Mark core access policies as final → (Closed) created by kristiaanvandeneynde
  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

    Done

  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium
  • Pipeline finished with Failed
    7 months ago
    Total: 632s
    #185767
  • 🇺🇸United States moshe weitzman Boston, MA

    as soon as one module chooses to extend over decorating, the gate is immediately closed for all other modules to do the same.

    If custom or contrib code makes a new policy and chooses to extend another (the original policy could be active or inactive - both valid use cases), why would all other active policies cease to work as designed? I must be missing something.

  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

    Okay, let's say you have a module that extends UserRolesAccessPolicy to the following:

    1. Change persistent cache contexts to one representing a sitewide flag
    2. In calculatePermissions(), if said flag is ON, return the parent and make sure we add user.roles context. If OFF, do nothing.
    3. In your service override file, set access_policy.user_roles class to your extending class

    Now we have another module that wants to do something else to UserRolesAccessPolicy and also decides to extend said class. Now how would that module work together with module A? They are both trying to replace alter container's definition of access_policy.user_roles to point to their class. Only one can win.

    With decorators, the same results would be achievable, but both modules would be able to make their modification to UserRolesAccessPolicy.

  • 🇺🇸United States moshe weitzman Boston, MA

    In my example I said "makes a new policy".

    Your example is generally about the drawbacks of altering services from other modules. We have already settled that concern with permitted, but not recommended (see italicized warning) . final takes this warning a lot further. Your example is a perfectly reasonable thing for a site to do, and the final makes it harder. Site developers don't need to care about multiple cooperating access policies.

    Many cooperating policies is a rare use case. We've learned this with node access grants system - we built a system which handles multiple cooperating policies but few understand the system. We would benefit from a simpler and less cooperative system here IMO. Sorry I'm going off topic here.

    IMO, this comes down to how paternal we want our code to be. I slightly prefer less paternal.

  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

    Ah right, making a new policy based on an existing one was also talked about on Slack: If you based your policy off of an existing one and leave the existing one intact, then you're not using the API properly.

    The original policy will provide a list of permissions and your policy will provide a highly likely very similar list because it used the original policy as its starting point. But all permissions are merged together at the end of the loop over all policies. So basing your access policy off of anything but the base class is a waste of computing resources because you're calculating something that's already been calculated.

    Same goes for the alterPermissions() call, it's intended to add or remove items after the calculate phase. If you were to extend an existing policy, you'd be running the same code twice even though this code only has to run once to be effective.

    Keep in mind a site can still swap out access policy services even if they're final. Simply decorate said service and never call the parent where you don't want to.

    IMO, this comes down to how paternal we want our code to be. I slightly prefer less paternal.

    Generally, me too. We have over 100 final classes in core and I don't necessarily agree with all of them. But this is where it really makes sense. Access policies should never overlap with one another, so there is absolutely no reason to extend one as a base of your own. Given how all methods are public by design, it's really built for decorating should the need ever arise.

    Having said that, with both a build and alter phase, you should rarely run into a scenario where you need to actually fiddle with someone else's policies.

  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

    Can we please commit this, though? As @catch has pointed out on Slack, we can always remove final whenever we want (even in a minor or patch release) and it would not constitute a BC break. Adding it, however, would require a new major release and a deprecation process.

    I consider the majority of the Access Policy API as part of the user subsystem of which I'm the maintainer along with Moshe. I'll gladly pledge here that if anyone at any point comes up with a real-life practical example that simply cannot be solved in any other way than to extend an existing policy, I'll gladly remove the final keyword for them. I'm willing to commit to that.

    They can point me to this very comment and make me eat it.

    So can we pretty please get this in before 10.3 is released?

  • Status changed to RTBC 7 months ago
  • 🇺🇸United States smustgrave

    If @catch said it can be removed later, don't mind marking.

    • catch committed 44521372 on 10.3.x
      Issue #3450576 by kristiaanvandeneynde: Access policies should be marked...
    • catch committed f1683a89 on 10.4.x
      Issue #3450576 by kristiaanvandeneynde: Access policies should be marked...
    • catch committed 6b322d85 on 11.0.x
      Issue #3450576 by kristiaanvandeneynde: Access policies should be marked...
    • catch committed 9935d24a on 11.x
      Issue #3450576 by kristiaanvandeneynde: Access policies should be marked...
  • 🇬🇧United Kingdom catch

    Yeah I am skeptical about final everywhere but where we really don't think something should be an extension point (because we know there are other, better, ones), it makes sense to use it. Note that 🌱 Use final to define classes that are NOT extension points Active is open to discuss where and how to use it.

    Not sure why there were 11.x and 10.3.x MRs here since they were identical, just cherry-picked the 11.x commit. Committed/pushed to 11.x and cherry-picked back to 10.3.x, thanks!

  • Status changed to Fixed 7 months ago
  • 🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

    Thanks! I'll make sure to update the documentation to reflect these changes come Monday.

  • Automatically closed - issue fixed for 2 weeks with no activity.

Production build 0.71.5 2024