diff --git a/composer.json b/composer.json index fbbe5d05..a29fbcd6 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,7 @@ "php": "^7.1.3", "ext-ctype": "*", "ext-iconv": "*", + "doctrine/annotations": "^1.6", "friendsofsymfony/ckeditor-bundle": "^2.0", "omines/datatables-bundle": "^0.2.2", "php-translation/symfony-bundle": "^0.8.1", diff --git a/composer.lock b/composer.lock index e70b8c33..d74f90af 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "66c8dc61f5af2f03d10c9c1d247549e6", + "content-hash": "c16d05fa5ba54398968ed9f503221d90", "packages": [ { "name": "doctrine/annotations", diff --git a/config/services.yaml b/config/services.yaml index b5471e22..9ab44ebd 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -28,5 +28,12 @@ services: resource: '../src/Controller' tags: ['controller.service_arguments'] + app.doctrine.elementListener: + class: App\Security\EntityListeners\ElementPermissionListener + public: false + autowire: true + tags: + - { name: "doctrine.orm.entity_listener" } + # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones diff --git a/src/Entity/DBElement.php b/src/Entity/DBElement.php index cece1561..78b17c60 100644 --- a/src/Entity/DBElement.php +++ b/src/Entity/DBElement.php @@ -34,6 +34,8 @@ use Doctrine\ORM\Mapping as ORM; * must have the table row "id"!! The ID is the unique key to identify the elements. * * @ORM\MappedSuperclass() + * + * @ORM\EntityListeners({"App\Security\EntityListeners\ElementPermissionListener"}) */ abstract class DBElement { diff --git a/src/Entity/Part.php b/src/Entity/Part.php index 0d5fc836..30753b11 100644 --- a/src/Entity/Part.php +++ b/src/Entity/Part.php @@ -34,6 +34,7 @@ namespace App\Entity; +use App\Security\Annotations\ColumnSecurity; use Doctrine\ORM\Mapping as ORM; //use Webmozart\Assert\Assert; @@ -109,6 +110,16 @@ class Part extends AttachmentContainingDBElement /** * @var string * @ORM\Column(type="string") + * + * @ColumnSecurity(prefix="name") + */ + protected $name; + + /** + * @var string + * @ORM\Column(type="string") + * + * @ColumnSecurity(prefix="description") */ protected $description = ""; @@ -116,6 +127,8 @@ class Part extends AttachmentContainingDBElement * @var int * @ORM\Column(type="integer") * @Assert\GreaterThanOrEqual(0) + * + * @ColumnSecurity(prefix="instock", type="integer") */ protected $instock = 0; @@ -123,12 +136,15 @@ class Part extends AttachmentContainingDBElement * @var int * @ORM\Column(type="integer") * @Assert\GreaterThanOrEqual(0) + * + * @ColumnSecurity(prefix="mininstock", type="integer") */ protected $mininstock = 0; /** * @var string * @ORM\Column(type="string") + * @ColumnSecurity(prefix="comment") */ protected $comment = ""; diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index bce4346c..757d01c0 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -38,5 +38,6 @@ use Doctrine\ORM\EntityRepository; class PartRepository extends EntityRepository { + //TODO } \ No newline at end of file diff --git a/src/Security/Annotations/ColumnSecurity.php b/src/Security/Annotations/ColumnSecurity.php new file mode 100644 index 00000000..4ec3a2e0 --- /dev/null +++ b/src/Security/Annotations/ColumnSecurity.php @@ -0,0 +1,110 @@ +prefix !== '') { + return $this->prefix . '.' . $this->read; + } + return $this->read; + } + + public function getEditOperationName() : string + { + if($this->prefix !== '') { + return $this->prefix . '.' . $this->edit; + } + + return $this->edit; + } + + public function getPlaceholder() + { + if($this->placeholder === null) + { + switch($this->type) + { + case 'integer': + return 0; + case 'string': + return '???'; + case 'object': + return null; + } + } + + return $this->placeholder; + } + +} \ No newline at end of file diff --git a/src/Security/EntityListeners/ElementPermissionListener.php b/src/Security/EntityListeners/ElementPermissionListener.php new file mode 100644 index 00000000..318509ac --- /dev/null +++ b/src/Security/EntityListeners/ElementPermissionListener.php @@ -0,0 +1,133 @@ +security = $security; + } + + + /** + * @PostLoad + * + * This function is called after doctrine filled, the entity properties with db values. + * We use this, to check if the user is allowed to access these properties, and if not, we write a placeholder + * into the element properties, so that a user only gets non sensitve data. + * + */ + public function postLoadHandler(DBElement $element, LifecycleEventArgs $event) + { + //Read Annotations and properties. + $reflectionClass = new ReflectionClass($element); + $properties = $reflectionClass->getProperties(); + $reader = new AnnotationReader(); + + foreach($properties as $property) + { + /** + * @var ColumnSecurity $annotation + */ + $annotation = $reader->getPropertyAnnotation($property, + ColumnSecurity::class); + + if($annotation !== null) + { + //Check if user is allowed to read info, otherwise apply placeholder + if(!$this->security->isGranted($annotation->getReadOperationName(), $element)) + { + $property->setAccessible(true); + $property->setValue($element, $annotation->getPlaceholder()); + } + } + } + } + + /** + * @PreUpdate + * This function is called before Doctrine saves the property values to the database. + * We use this function to revert the changes made in postLoadHandler(), so nothing gets changed persistently. + */ + public function preUpdateHandler(DBElement $element, PreUpdateEventArgs $event) + { + $reflectionClass = new ReflectionClass($element); + $properties = $reflectionClass->getProperties(); + $reader = new AnnotationReader(); + + foreach($properties as $property) + { + /** + * @var ColumnSecurity $annotation + */ + $annotation = $reader->getPropertyAnnotation($property, + ColumnSecurity::class); + + if($annotation !== null) + { + $field_name = $property->getName(); + + //Check if user is allowed to edit info, otherwise overwrite the new value + // so that nothing is changed in the DB. + if($event->hasChangedField($field_name) && + !$this->security->isGranted($annotation->getEditOperationName(), $element)) + { + $event->setNewValue($field_name, $event->getOldValue($field_name)); + } + } + } + } + + +} \ No newline at end of file