typo3/access-control

TYPO3 Access Control Component.

0.1.1 2019-12-04 16:05 UTC

This package is auto-updated.

Last update: 2024-11-06 06:21:42 UTC


README

Build Coverage Code Quality

Installation

Use composer to install this extension in your project:

composer require typo3/access-control

Concepts

Access rights are granted to users through the use of policies. The underlying model is known as attribute-based access control (ABAC). It makes use of boolean expressions which decide whether an access request is granted or not. Such a request typically contains the resource, action, subject and environment attributes. This extension implements a lightweight policy language and evaluation framework based on Jiang, Hao & Bouabdallah, Ahmed (2017).

The policy structure consists of policy sets, policies and rules. A policy set is a set of policies which in turn has a set of rules. Because not all policies are relevant to a given request every element includes the notion of a target. It determines whether a policy is applicable to a request by setting constraints on attributes using boolean expressions.

A policy is applicable if the access request satisfies the target. If so, its childrend are evaluated and the results returned by those children are combined using a combining algorithm. Otherwise, the policy is skipped without further examining its children and returns a not applicable decision.

The rule is the fundamental unit that can generate a conclusive decision. The condition of a rule is a more complex boolean expression that refines the applicability beyond the predicates specified by its target, and is optional. If a request satisfies both the target and condition of a rule, then the rule is applicable to the request and its effect is returned as its decision. Otherwise, not applicable is returned.

Each rule, policy or policy set has an unique identifier and obligations which represent the operations to perform after granting or denying an access request.

Attributes

A request typically contains the following attributes:

To define your own attributes, you must derive them from one of the corresponding classes:

namespace App\Security\AccessControl\Attribute;

use TYPO3\AccessControl\Attribute\PrincipalAttribute;

class RoleAttribute extends PrincipalAttribute
{
  public function __construct(string $identifier)
  {
    parent::__construct($identifier);
  }
}

Expressions

Expressions are used to decied whether a policy is applicable to a request or not. Therefore a so called expression resolver has to be implemented. For example, by using the expression language component:

namespace App\Security\AccessControl\Expression;

use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use TYPO3\AccessControl\Expression\ResolverInterface;

class ExpressionLanguageResolver implements ResolverInterface
{
  private $expressionLanguage;

  public function __construct()
  {
    $this->expressionLanguage = new ExpressionLanguage();
    // register a custom function `hasAuthority`
    $this->expressionLanguage->register(
      'hasAuthority', function () {
        // not implemented, we only use the evaluator
      },
      function ($variables, ...$arguments) {
        if (count($arguments) == 1) {
          // checks if the subject has the given principal
          return isset($variables['subject']->principals[$arguments[0]]);
        }
        return false;
      }
    );
  }

  public function validate(string $expression): void
  {
    // only allow the attributes `subject`, `resource` and `action`
    $this->expressionLanguage->parse($expression, ['subject', 'resource', 'action']);
  }

  public function evaluate(string $expression, array $attributes): bool
  {
    return $this->expressionLanguage->evaluate($expression, $attributes);
  }
}

Policies

Policies can be defined declaratively. For example, by using YAML and the policy factory:

---
description: 'Root policy set.'
algorithm: highestPriority
policies:
  Admin:
    target: 'hasAuthority("typo3:security:principal:admin")'
    description: 'Administrator policy'
    priority: 100
    rules:
      -
        effect: permit
  Default:
    description: 'Deny everything per default.'
    rules:
      -
        obligation:
          deny:
            Feedback: ['Access denied.']
use App\Security\AccessControl\Expression\ExpressionLanguageResolver;
use Symfony\Component\Yaml\Parser;
use TYPO3\AccessControl\Policy\PolicyFactory;

$resolver = new ExpressionLanguageResolver();
$factory = new PolicyFactory();
$parser = new Parser();

$policy = $factory->build(
  $parser->parseFile('/path/to/policies.yaml'),
  $resolver
);

A policy set is a set of policy sets and policies. It has the following configuration fields:

With configuration fields similar to a policy set a policy is a set of rules:

Unlike a policy set or a policy, a rule does not contain any leaf nodes:

Policies may conflict and produce different decisions for the same request. To resolve this four kinds of combining algorithms are provided. Each algorithm represents a different way for combining multiple local decisions into a single global decision:

Please note that for all of these combining algorithms, not applicable is returned if not any of the children is applicable.

There is also formal description of the schema for the policy language which can be found here.

Authorisation

To perform an access request the policy decision point has to be used. It evaluates all policies and returns a decision either of permit, deny or not applicable:

use App\Security\AccessControl\Attribute\ActionAttribute;
use App\Security\AccessControl\Attribute\ResourceAttribute;
use Symfony\Component\EventDispatcher\EventDispatcher;
use TYPO3\AccessControl\Policy\PolicyDecisionPoint;
use TYPO3\AccessControl\Policy\PolicyInformationPoint;

$dispatcher = new EventDispatcher();

// creeates an policy information point
$policyInformationPoint = new PolicyInformationPoint(
  $dispatcher
);

// creates a policy decision point
$policyDecisionPoint = new PolicyDecisionPoint(
  $dispatcher,
  $policy,
  $policyInformationPoint
);

// perform an authorization request
$policyDecision = $policyDecisionPoint->authorize(
  [
    // concrete resource to access
    'resource' => new ResourceAttribute('identifier'),
    // concrete action on resource
    'action' => new ActionAttribute()
  ]
);

if (!$policyDecision->isApplicable()) {
  // access request is not applicable
}

// process determining policy rule
$determinigRule = $policyDecision->getRule();

foreach ($policyDecision->getObligations() as $obligation) {
  // process obligations
}

if ($policyDecision->getValue() === PolicyDecision::PERMIT)
  // access is granted
}

// access is denied otherwise

To receive all operations which should be performed after denying or granting an access request the event \TYPO3\AccessControl\Event\PolicyDecisionEvent has to be used:

namespace App\Security\AccessControl\EventListener;

use TYPO3\AccessControl\Event\PolicyDecisionEvent;

class PolicyDecisionListener
{
    public function __invoke(PolicyDecisionEvent $event)
    {
        // ...
    }
}

To provide additional data for an attribute before an access request the event \TYPO3\AccessControl\Event\AttributeRetrievalEvent can be used:

namespace App\Security\AccessControl\EventListener;

use TYPO3\AccessControl\Event\AttributeRetrievalEvent;

class AttributeRetrievalListener
{
    public function __invoke(AttributeRetrievalEvent $event)
    {
        // ...
    }
}

To provide principals for the subject attribute the separate event \TYPO3\AccessControl\Event\SubjectRetrievalEvent has to be used:

namespace App\Security\AccessControl\EventListener;

use TYPO3\AccessControl\Attribute\PrincipalAttribute;
use TYPO3\AccessControl\Event\SubjectRetrievalEvent;

class SubjectRetrievalListener
{
    public function __invoke(SubjectRetrievalEvent $event)
    {
        // Adds administrator principal
        $event->addPrincipal(new PrincipalAttribute('administrator'));
    }
}

Design Principals

Whenever possible the authorization logic should be part of a policy. Thus it's auditable and changeable. For reasons of performance or complexity it might be not possible. Then it's recommended to extend the expression language with a custom function if possible.

Development

Development for this extension is happening as part of the TYPO3 persistence initiative.