demos-europe/edt-access-definitions

Tools to regulate access to entities and their properties.


README

Allows fine-grained definitions how entities and their properties are allowed to be accessed in CRUD applications.

Provides PHP classes that build upon the query component to provide the means to enforce access limitation and simple aliasing from the wrapper schema to the schema of your backing object. The path-building component can optionally be used to ease the usage depending on the use case.

Overview

This component provides the means to define so called Types for corresponding objects in your application. Your Types will limit the access to the schema and instances of your objects depending on the authorization of the accessing user or other states in your application. It shows its main advantages in CRUD applications but can be applied to other types of applications as well.

As an example lets assume a very simple CMS software. It has an Article class which is connected to its author in a bidirectional many-to-one relationship.

Querying

Suppose for your business layer you need to distinguish between articles which are in their draft state and thus only visible to their authors and finished articles visible to everyone. Instead of sprinkling your business layer with potentially duplicated checks which user is allowed to access which articles you can centralize your authorisations in an ArticleType class.

After the initial setup you can use it with conditions to query instances from your data source, similar to the Query Component:

use EDT\Wrapping\Contracts\Types\TransferableTypeInterface;
use EDT\ConditionFactory\ConditionFactoryInterface;

function getArticleType(): ArticleType {
    // returns the Type defining access limitations to your Article objects
}
function getEntityProvider(): EntityProviderInterface {
    // returns the instance to access your data source
}
function getConditionFactory(): ConditionFactoryInterface {
    // returns a factory for conditions adjusted to your data source
}

// initializations
$articleType = getArticleType();
$conditionFactory = getConditionFactory();
$typeProvider = new PrefilledTypeProvider([$articleType]);
$pathProcessor = new SchemaPathProcessor(new PropertyPathProcessorFactory(), $typeProvider);

// create a condition for your business logic
$nameCondition = $conditionFactory->propertyHasValue('jacob', 'author', 'accountName');

// check if the path used in the condition is allowed to be accessed
// and resolve aliases if used in the path
$pathProcessor->mapFilterConditions($articleType, [$nameCondition]);
// add the access condition defined by the type itself
$conditions[] = $pathProcessor->processAccessCondition($articleType);

// get the entities matching not only the name condition
// but also all conditions defined by the article type itself
$filteredEntities = $entityFetcher->getEntities($conditions);

The three helper function are left empty because their implementation depends on your actual use case:

  1. getArticleType returns your ArticleType which you can implement by extending TransferableTypeInterface and instantiate in your favorite style (manually, as Symfony Service, loaded by configuration, …​). An overview for the different interfaces available and how to implement them is shown in How to implement types.

  2. The instances returned by getEntityProvider and getConditionFactory depend on your data source. For possible classes see Providers and Factories setup.

As you can see in this example we explicitly specified a condition to only get articles written by jacob. However, if the ArticleType was configured correctly we will actually get a subset of the result with only those articles that the current user is allowed to access. E.g. if the user currently logged in is not jacob she or he will only receive articles that are not in their draft state anymore. jacob on the other hand well get all his articles. This implicit condition will be executed automatically and only needs to be set once in the ArticleType, where it will be applied every time the ArticleType is used to access Article objects.

Wrapping

If the object class you’ve written your Type for doesn’t contain any relationships but primitive types only then accessing your actual object instances may be fine. However, in the case of the Article objects we need to prevent users from simply getting an arbitrary article available to them via the ArticleType and then accessing its actual author instance. This would enable unlimited access to all Article instances of that author, regardless of any setting in the ArticleType.

Because of this you can wrap your entity instances with a corresponding type. The properties the wrapper grants access to depend on the ArticleType and its relations to other Types. For example instead of completely denying access to the author in the ArticleType we may want to configure a UserType. Like the ArticleType the UserType can restrict access to data and schema. This way we can allow access to authors to get their public name, but can prevent the access to their drafts.

function getWrapperFactory(): WrapperObjectFactory {
    // the wrapper factory initialized with the necessary parameters
}

$wrapperFactory = getWrapperFactory();
$wrappedEntities = array_map(function (Article $articleEntity) use ($articleType): WrapperObject {
    return $this->getWrapperFactory()->createWrapper($articleEntity, $articleType);
}, $articleEntities);

Also note that the restrictions of Types are in place no matter how they are accessed. For example when you’ve restricted access to the internal e-mail address of your users using a UserType then it does not matter if the wrapper of the User object was received via a relationship from a wrapped Article`, a wrapped Comment or wrapped manually, the restriction will always be applied.

Credits and acknowledgements

Conception and implementation by Christian Dressler with many thanks to eFrane.