Add the access policy API

Created on 25 July 2023, over 1 year ago
Updated 18 November 2023, 12 months ago

Part of 🌱 [Meta, Plan] Pitch-Burgh: Policy based access in core Active

Summary

We want to convert the Flexible Permissions module into core code. This will allow for any access policy to be translated into a set of permissions so that access checks can run. 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.

This value object can then be used by a permission checker (such as the one introduced here Centralize permission checks in a service Fixed ) to verify if an account has permission to a given access check.

A key aspect that may not seem useful to core is the use of scopes. By default, all access policies will return permissions for the "drupal" scope, but to make it so access modules in contrib don't have to copy all of this logic, the use of scopes will allow them to define permissions in other scopes, such as Group, Domain, Commerce Stores, etc. where the calculated permissions will only have effect within the scope of, say, one domain or group.

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.

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 (part of core) 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 new API but I'll explain it here rather than the implementation issue. How do you use these built permissions? Well, you use a central permission checker such as the one introduced in Centralize permission checks in a service Fixed 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);
    }
📌 Task
Status

Fixed

Version

11.0 🔥

Component
Base 

Last updated about 6 hours ago

Created by

🇧🇪Belgium kristiaanvandeneynde Antwerp, Belgium

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

Comments & Activities

Production build 0.71.5 2024