Summary
A new access policy API has been added to core, allowing you to assign permissions through numerous ways rather than just user roles (or UID1). Each policy is a tagged service that can add or remove permissions for a particular user, based on globally available context data such as the domain, time of day, current user's field values, etc.
These permissions are calculated once during a build phase and then pulled through an alter phase. After both phases are complete, the end result is poured into an immutable value object and cached.
Because access policies are services, you can also disable, swap out or decorate another module's or even core's access policies. So with #3376846: Implement the new access policy API committed, you could now disable the fact that user 1 has an all access pass.
Overview of new API
New concept: Build and alter phase
The goal is to loop over all access policies in a build phase and come up with a set of permissions that are from then on immutable. These permissions respect cache contexts (and are thus added to the variation cache), so you could be getting a different set depending on the time of day, your user roles, etc. The reason the permissions have to be immutable is because we still cache everything by user.permissions and if your permissions could change during runtime, that would quickly become a security nightmare.
Right before we turn the built permissions into this immutable object, however, we allow all policies to have a final alter pass of the fully built permissions. This adds some extra flexibility for people who want to alter other modules' (or core's) access policy behavior from the outside. After the alter pass, the immutable object is built and cached.
New concept: Scopes and identifiers
Furthermore, these permissions are built for a given scope and identifier within said scope. For Drupal core, both of these simply default to AccessPolicyInterface::SCOPE_DRUPAL and seemingly do nothing, but it's contrib where the addition of this concept will truly shine. You see, for Group, Domain, or similar we don't necessarily care about what permissions you have across the entire website, but rather what you can do within a subset, e.g. a single domain or group.
The main question then becomes "To what do these permissions apply?" Because Group is a bit complex to explain here (it has 3 scopes), let's use Domain as an example: If you want to discern who can make changes to content on an individual domain (whether active or not), then you would hand out those permissions in SCOPE_DOMAIN, where the identifier is the machine name of the domain. Doing this would for example allow the "Belgian team" to change the content of the ".be" website, but not the ".nl".
AccessPolicyProcessor and VariationCache
This is where the AccessPolicyProcessor (APP) and VariationCache (see this change record) come into play. The APP is a service collector that looks for services which are tagged as access_policy. It then asks all of these policies what they initially vary by (i.e. cache contexts) and evaluates what they end up varying by at the end of the permission processing. This concept of "initial cache contexts" and "final cache contexts" is what powers cache redirects and is required to store something in the variation cache.
Before any of this runs, however, it first asks each individual access policy whether it applies to the scope and does not do anything with those that don't. So any Group or Domain specific access policies will not interfere with your sitewide Drupal permissions or vice versa.
(Refinable)CalculatedPermissions and CalculatedPermissionsItem
These value objects simply represent all of what was described above. The CalculatedPermissions object holds entries for an infinite amount of identifiers across an infinite amount of scopes. Keep in mind: In case of Drupal core it would be one scope and one identifier.
At each scope-identifier address sits a CalculatedPermissionsItem that indicates what permissions you should get for a given identifier within a scope and whether or not you have admin rights (i.e. all permissions) there. If you try to add multiple CPI to the same address, they get merged or overwritten depending on a parameter in the RefinableCalculatedPermissions::addItem() method.
Which leads to the next point: The difference between RefinableCalculatedPermissions and CalculatedPermissions. Simply put, the former allows you to make changes and is passed around during the build phase, the latter is immutable and passed around to those services who request what your fully built permissions are.
Permission checkers
This is not part of the API per se but rather the core implementation (#3376846: Implement the new access policy API). Let's explain it here anyway so that we can paint a complete picture. How do you use these built permissions? Well, you use a central permission checker such as the one introduced in #3347873: Centralize permission checks in a service and call the AccessPolicyProcessor. Then you ask for the permission item at the scope-identifier (SCOPE_DRUPAL/SCOPE_DRUPAL for core) address you seek and simply run hasPermission() on it.
Here's what that would look like (taken from the implementation issue):
/**
* {@inheritdoc}
*/
public function hasPermission(string $permission, AccountInterface $account): bool {
$item = $this->processor->processAccessPolicies($account)->getItem();
return $item && $item->hasPermission($permission);
}
Sane defaults
See anything missing in the example above? We didn't have to specify AccessPolicyInterface::SCOPE_DRUPAL anywhere because the system defaults to the drupal scope and identifier. Contrib scopes would have to specify these parameters in the processAccessPolicies() and getItem() call.
To make life easier on people wanting to add an access policy to core from within a contrib module (such as office hours), CalculatedPermissionsItem also defaults to adding permissions to the SCOPE_DRUPAL/SCOPE_DRUPAL address. This should make access policies aimed at core easier on the eyes.
Here's what that would look like (taken from the implementation issue):
foreach ($user_roles as $user_role) {
$calculated_permissions
->addItem(new CalculatedPermissionsItem($user_role->getPermissions(), $user_role->isAdmin()))
->addCacheableDependency($user_role);
}
This work was funded by the PitchBurgh innovation contest
Comments
I've read through this but it
I've read through this but it introduces a new concepts and I'm having a hard time understanding what this gives us that the existing permissions and roles system doesn't. Can some practical examples be presented?
+1
Thanks @bkosborne, totally agree. Wondering why I didn't see this big one before.
Especially it would be interesting to hear, if it will push things forward in the topic of access records / Node-specific things. Especially in views it's still painful not to be able to hide custom entities the user isn't allowed to access in a clean way. (I know why, yes...)
Really looking forward to general API solutions on these framework topics, which still seem unsolved today for this huge and powerful framework.
But nevertheless: Thank you very much @everyone who pushed things forward in this issue!
Especially @kristiaanvandeneynde! :)
http://www.DROWL.de || Professionelle Drupal Lösungen aus Ostwestfalen-Lippe (OWL)
http://www.webks.de || webks: websolutions kept simple - Webbasierte Lösungen die einfach überzeugen!
http://www.drupal-theming.com || Individuelle Responsive Themes
I guess the description for
I guess the description for flexible_permissions may help to better understand what PBAC does: https://www.drupal.org/project/flexible_permissions
Permissions can come from anywhere now
Currently, your permissions come from the fact that you are user 1 or that you have certain user roles assigned. This is very limiting as to where your permissions can come from. Now, your permissions can come from anywhere:
Try getting the above to work with the old system, it would be absolutely horrible if not outright impossible to pull off. With Access Policy API, all you need is a single service (and perhaps a cache context) and you're done.
What's the benefit compared
What's the benefit compared to access handlers, e.g. for routes, entities or fields?
Imagine you have a module
Imagine you have a module that checks for a permission to "manage the webshop", you have the following:
All of these check for the permission "manage the webshop", whether via route access, permission checks in controllers, entity access, ...
Good luck trying to adjust all of that logic to account for the scenario: "Managers may not change the webshop during the weekend if they're in their trial period". You'd have to copy the same logic to all these different places.
With access policies, you'd simply write one access policy that revokes the "manage the webshop" permission under these conditions and all of these access checks across your website would instantly pick up on the change.